github.com/jpreese/tflint@v0.19.2-0.20200908152133-b01686250fb6/tflint/loader.go (about)

     1  package tflint
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"log"
     8  	"os"
     9  	"sort"
    10  	"strings"
    11  
    12  	version "github.com/hashicorp/go-version"
    13  	hcl "github.com/hashicorp/hcl/v2"
    14  	"github.com/hashicorp/hcl/v2/hclparse"
    15  	"github.com/hashicorp/hcl/v2/hclsyntax"
    16  	"github.com/hashicorp/terraform/addrs"
    17  	"github.com/hashicorp/terraform/configs"
    18  	"github.com/hashicorp/terraform/terraform"
    19  	"github.com/spf13/afero"
    20  )
    21  
    22  //go:generate go run github.com/golang/mock/mockgen -source loader.go -destination loader_mock.go -package tflint -self_package github.com/terraform-linters/tflint/tflint
    23  
    24  // AbstractLoader is a loader interface for mock
    25  type AbstractLoader interface {
    26  	LoadConfig(string) (*configs.Config, error)
    27  	LoadAnnotations(string) (map[string]Annotations, error)
    28  	LoadValuesFiles(...string) ([]terraform.InputValues, error)
    29  	Files() (map[string]*hcl.File, error)
    30  	Sources() map[string][]byte
    31  }
    32  
    33  // Loader is a wrapper of Terraform's configload.Loader
    34  type Loader struct {
    35  	parser               *configs.Parser
    36  	fs                   afero.Afero
    37  	currentDir           string
    38  	config               *Config
    39  	moduleSourceVersions map[string][]*version.Version
    40  	moduleManifest       map[string]*moduleManifest
    41  }
    42  
    43  type moduleManifest struct {
    44  	Key        string           `json:"Key"`
    45  	Source     string           `json:"Source"`
    46  	Version    *version.Version `json:"-"`
    47  	VersionStr string           `json:"Version,omitempty"`
    48  	Dir        string           `json:"Dir"`
    49  }
    50  
    51  type moduleManifestFile struct {
    52  	Modules []*moduleManifest `json:"Modules"`
    53  }
    54  
    55  // NewLoader returns a loader with module manifests
    56  func NewLoader(fs afero.Afero, cfg *Config) (*Loader, error) {
    57  	log.Print("[INFO] Initialize new loader")
    58  
    59  	l := &Loader{
    60  		parser:               configs.NewParser(fs),
    61  		fs:                   fs,
    62  		config:               cfg,
    63  		moduleSourceVersions: map[string][]*version.Version{},
    64  		moduleManifest:       map[string]*moduleManifest{},
    65  	}
    66  
    67  	if _, err := os.Stat(getTFModuleManifestPath()); !os.IsNotExist(err) {
    68  		log.Print("[INFO] Module manifest file found. Initializing...")
    69  		if err := l.initializeModuleManifest(); err != nil {
    70  			log.Printf("[ERROR] %s", err)
    71  			return nil, err
    72  		}
    73  	}
    74  
    75  	return l, nil
    76  }
    77  
    78  // LoadConfig loads Terraform's configurations
    79  // TODO: Can we use configload.LoadConfig instead?
    80  func (l *Loader) LoadConfig(dir string) (*configs.Config, error) {
    81  	l.currentDir = dir
    82  	log.Printf("[INFO] Load configurations under %s", dir)
    83  	rootMod, diags := l.parser.LoadConfigDir(dir)
    84  	if diags.HasErrors() {
    85  		log.Printf("[ERROR] %s", diags)
    86  		return nil, diags
    87  	}
    88  
    89  	if !l.config.Module {
    90  		log.Print("[INFO] Module inspection is disabled. Building a root module without children...")
    91  		cfg, diags := configs.BuildConfig(rootMod, l.ignoreModuleWalker())
    92  		if diags.HasErrors() {
    93  			return nil, diags
    94  		}
    95  		return cfg, nil
    96  	}
    97  	log.Print("[INFO] Module inspection is enabled. Building a root module with children...")
    98  
    99  	cfg, diags := configs.BuildConfig(rootMod, l.moduleWalker())
   100  	if !diags.HasErrors() {
   101  		return cfg, nil
   102  	}
   103  
   104  	log.Printf("[ERROR] Failed to load modules: %s", diags)
   105  	return nil, diags
   106  }
   107  
   108  // Files returns a map of hcl.File pointers for every file that has been read by the loader.
   109  // It uses the source cache to avoid re-loading the files from disk. These files can be used
   110  // to do low level decoding of Terraform configuration.
   111  func (l *Loader) Files() (map[string]*hcl.File, error) {
   112  	sources := l.parser.Sources()
   113  	result := make(map[string]*hcl.File, len(sources))
   114  	parser := hclparse.NewParser()
   115  
   116  	for path, src := range sources {
   117  		var file *hcl.File
   118  		var diags hcl.Diagnostics
   119  		switch {
   120  		case strings.HasSuffix(path, ".json"):
   121  			file, diags = parser.ParseJSON(src, path)
   122  		default:
   123  			file, diags = parser.ParseHCL(src, path)
   124  		}
   125  
   126  		if diags.HasErrors() {
   127  			return nil, diags
   128  		}
   129  
   130  		result[path] = file
   131  	}
   132  
   133  	return result, nil
   134  }
   135  
   136  // LoadAnnotations load TFLint annotation comments as HCL tokens.
   137  func (l *Loader) LoadAnnotations(dir string) (map[string]Annotations, error) {
   138  	primary, override, diags := l.parser.ConfigDirFiles(dir)
   139  	if diags != nil {
   140  		log.Printf("[ERROR] %s", diags)
   141  		return nil, diags
   142  	}
   143  	configFiles := append(primary, override...)
   144  
   145  	ret := map[string]Annotations{}
   146  
   147  	for _, configFile := range configFiles {
   148  		if !strings.HasSuffix(configFile, ".tf") {
   149  			continue
   150  		}
   151  
   152  		src, err := l.fs.ReadFile(configFile)
   153  		if err != nil {
   154  			return nil, err
   155  		}
   156  		tokens, diags := hclsyntax.LexConfig(src, configFile, hcl.Pos{Byte: 0, Line: 1, Column: 1})
   157  		if diags.HasErrors() {
   158  			return nil, diags
   159  		}
   160  		ret[configFile] = NewAnnotations(tokens)
   161  	}
   162  
   163  	return ret, nil
   164  }
   165  
   166  // LoadValuesFiles reads Terraform's values files and returns terraform.InputValues list in order of priority
   167  // Pass values ​​files specified from the CLI as the arguments in order of priority
   168  // This is the responsibility of the caller
   169  func (l *Loader) LoadValuesFiles(files ...string) ([]terraform.InputValues, error) {
   170  	log.Print("[INFO] Load values files")
   171  
   172  	values := []terraform.InputValues{}
   173  
   174  	for _, file := range files {
   175  		if _, err := os.Stat(file); os.IsNotExist(err) {
   176  			return values, fmt.Errorf("`%s` is not found", file)
   177  		}
   178  	}
   179  
   180  	autoLoadFiles, err := l.autoLoadValuesFiles()
   181  	if err != nil {
   182  		log.Printf("[ERROR] %s", err)
   183  		return nil, err
   184  	}
   185  	if _, err := os.Stat(defaultValuesFile); !os.IsNotExist(err) {
   186  		autoLoadFiles = append([]string{defaultValuesFile}, autoLoadFiles...)
   187  	}
   188  
   189  	for _, file := range autoLoadFiles {
   190  		vals, err := l.loadValuesFile(file, terraform.ValueFromAutoFile)
   191  		if err != nil {
   192  			return nil, err
   193  		}
   194  		values = append(values, vals)
   195  	}
   196  	for _, file := range files {
   197  		vals, err := l.loadValuesFile(file, terraform.ValueFromNamedFile)
   198  		if err != nil {
   199  			return nil, err
   200  		}
   201  		values = append(values, vals)
   202  	}
   203  
   204  	return values, nil
   205  }
   206  
   207  // Sources returns the source code cache for the underlying parser of this loader
   208  func (l *Loader) Sources() map[string][]byte {
   209  	return l.parser.Sources()
   210  }
   211  
   212  // autoLoadValuesFiles returns all files which match *.auto.tfvars present in the current directory
   213  // The list is sorted alphabetically. This is equivalent to priority
   214  // Please note that terraform.tfvars is not included in this list
   215  func (l *Loader) autoLoadValuesFiles() ([]string, error) {
   216  	files, err := l.fs.ReadDir(".")
   217  	if err != nil {
   218  		return nil, err
   219  	}
   220  
   221  	ret := []string{}
   222  	for _, file := range files {
   223  		if file.IsDir() {
   224  			continue
   225  		}
   226  
   227  		if strings.HasSuffix(file.Name(), ".auto.tfvars") || strings.HasSuffix(file.Name(), ".auto.tfvars.json") {
   228  			ret = append(ret, file.Name())
   229  		}
   230  	}
   231  	sort.Strings(ret)
   232  
   233  	return ret, nil
   234  }
   235  
   236  func (l *Loader) loadValuesFile(file string, sourceType terraform.ValueSourceType) (terraform.InputValues, error) {
   237  	log.Printf("[INFO] Load `%s`", file)
   238  	vals, diags := l.parser.LoadValuesFile(file)
   239  	if diags.HasErrors() {
   240  		log.Printf("[ERROR] %s", diags)
   241  		if diags[0].Subject == nil {
   242  			// HACK: When Subject is nil, it outputs unintended message, so it replaces with actual file.
   243  			return nil, errors.New(strings.Replace(diags.Error(), "<nil>: ", fmt.Sprintf("%s: ", file), 1))
   244  		}
   245  		return nil, diags
   246  	}
   247  
   248  	ret := make(terraform.InputValues)
   249  	for k, v := range vals {
   250  		ret[k] = &terraform.InputValue{
   251  			Value:      v,
   252  			SourceType: sourceType,
   253  		}
   254  	}
   255  	return ret, nil
   256  }
   257  
   258  func (l *Loader) moduleWalker() configs.ModuleWalker {
   259  	return configs.ModuleWalkerFunc(func(req *configs.ModuleRequest) (*configs.Module, *version.Version, hcl.Diagnostics) {
   260  		key := req.Path.String()
   261  		record, ok := l.moduleManifest[key]
   262  		if !ok {
   263  			log.Printf("[DEBUG] Failed to search by `%s` key.", key)
   264  			return nil, nil, hcl.Diagnostics{
   265  				{
   266  					Severity: hcl.DiagError,
   267  					Summary:  fmt.Sprintf("`%s` module is not found. Did you run `terraform init`?", req.Name),
   268  					Subject:  &req.CallRange,
   269  				},
   270  			}
   271  		}
   272  
   273  		log.Printf("[DEBUG] Trying to load the module: key=%s, version=%s, dir=%s", key, record.VersionStr, record.Dir)
   274  
   275  		mod, diags := l.parser.LoadConfigDir(record.Dir)
   276  		return mod, record.Version, diags
   277  	})
   278  }
   279  
   280  func (l *Loader) ignoreModuleWalker() configs.ModuleWalker {
   281  	return configs.ModuleWalkerFunc(func(req *configs.ModuleRequest) (*configs.Module, *version.Version, hcl.Diagnostics) {
   282  		return nil, nil, nil
   283  	})
   284  }
   285  
   286  func (l *Loader) initializeModuleManifest() error {
   287  	file, err := l.fs.ReadFile(getTFModuleManifestPath())
   288  	if err != nil {
   289  		return err
   290  	}
   291  	log.Printf("[DEBUG] Parsing the module manifest file: %s", file)
   292  
   293  	var manifestFile moduleManifestFile
   294  	err = json.Unmarshal(file, &manifestFile)
   295  	if err != nil {
   296  		return err
   297  	}
   298  
   299  	for _, m := range manifestFile.Modules {
   300  		if m.VersionStr != "" {
   301  			m.Version, err = version.NewVersion(m.VersionStr)
   302  			if err != nil {
   303  				return err
   304  			}
   305  			l.moduleSourceVersions[m.Source] = append(l.moduleSourceVersions[m.Source], m.Version)
   306  		}
   307  
   308  		moduleAddr := addrs.Module(strings.Split(m.Key, "."))
   309  		l.moduleManifest[moduleAddr.String()] = m
   310  	}
   311  
   312  	return nil
   313  }