github.com/mcuckson/tflint@v0.8.0/tflint/loader.go (about)

     1  package tflint
     2  
     3  import (
     4  	"crypto/md5"
     5  	"encoding/hex"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"log"
    11  	"os"
    12  	"path/filepath"
    13  	"sort"
    14  	"strings"
    15  
    16  	version "github.com/hashicorp/go-version"
    17  	"github.com/hashicorp/hcl2/hcl"
    18  	"github.com/hashicorp/terraform/configs"
    19  	"github.com/hashicorp/terraform/configs/configload"
    20  	"github.com/hashicorp/terraform/terraform"
    21  )
    22  
    23  //go:generate mockgen -source loader.go -destination ../mock/loader.go -package mock
    24  
    25  // AbstractLoader is a loader interface for mock
    26  type AbstractLoader interface {
    27  	LoadConfig() (*configs.Config, error)
    28  	LoadValuesFiles(...string) ([]terraform.InputValues, error)
    29  	IsConfigFile(string) bool
    30  }
    31  
    32  // Loader is a wrapper of Terraform's configload.Loader
    33  type Loader struct {
    34  	loader               *configload.Loader
    35  	configFiles          []string
    36  	moduleSourceVersions map[string][]*version.Version
    37  	moduleManifest       map[string]*moduleManifest
    38  }
    39  
    40  type moduleManifest struct {
    41  	Key        string           `json:"Key"`
    42  	Source     string           `json:"Source"`
    43  	Version    *version.Version `json:"-"`
    44  	VersionStr string           `json:"Version,omitempty"`
    45  	Dir        string           `json:"Dir"`
    46  	Root       string           `json:"Root"`
    47  }
    48  
    49  type moduleManifestFile struct {
    50  	Modules []*moduleManifest `json:"Modules"`
    51  }
    52  
    53  // NewLoader returns a loader with module manifests
    54  func NewLoader() (*Loader, error) {
    55  	log.Print("[INFO] Initialize new loader")
    56  
    57  	loader, err := configload.NewLoader(&configload.Config{
    58  		ModulesDir: getTFModuleDir(),
    59  	})
    60  	if err != nil {
    61  		log.Printf("[ERROR] %s", err)
    62  		return nil, err
    63  	}
    64  
    65  	primary, override, diags := loader.Parser().ConfigDirFiles(".")
    66  	if diags != nil {
    67  		log.Printf("[ERROR] %s", diags)
    68  		return nil, diags
    69  	}
    70  
    71  	l := &Loader{
    72  		loader:               loader,
    73  		configFiles:          append(primary, override...),
    74  		moduleSourceVersions: map[string][]*version.Version{},
    75  		moduleManifest:       map[string]*moduleManifest{},
    76  	}
    77  
    78  	if _, err := os.Stat(getTFModuleManifestPath()); !os.IsNotExist(err) {
    79  		log.Print("[INFO] Module manifest file found. Initializing...")
    80  		if err := l.initializeModuleManifest(); err != nil {
    81  			log.Printf("[ERROR] %s", err)
    82  			return nil, err
    83  		}
    84  	}
    85  
    86  	return l, nil
    87  }
    88  
    89  // LoadConfig loads Terraform's configurations
    90  // TODO: Can we use configload.LoadConfig instead?
    91  func (l *Loader) LoadConfig() (*configs.Config, error) {
    92  	log.Print("[INFO] Load configurations under the current directory")
    93  	rootMod, diags := l.loader.Parser().LoadConfigDir(".")
    94  	if diags.HasErrors() {
    95  		log.Printf("[ERROR] %s", diags)
    96  		return nil, diags
    97  	}
    98  
    99  	log.Print("[DEBUG] Trying to load modules using the legacy module walker...")
   100  	cfg, diags := configs.BuildConfig(rootMod, l.moduleWalkerLegacy())
   101  	if !diags.HasErrors() {
   102  		return cfg, nil
   103  	}
   104  	log.Print("[DEBUG] Failed to load modules using the legacy module walker; Trying the v0.10.6 module walker...")
   105  	log.Printf("[DEBUG] Original error: %s", diags)
   106  
   107  	cfg, diags = configs.BuildConfig(rootMod, l.moduleWalkerV0_10_6())
   108  	if !diags.HasErrors() {
   109  		return cfg, nil
   110  	}
   111  	log.Print("[DEBUG] Failed to load modules using the v0.10.6 module walker; Trying the v0.10.7 ~ v0.10.8 module walker...")
   112  	log.Printf("[DEBUG] Original error: %s", diags)
   113  
   114  	cfg, diags = configs.BuildConfig(rootMod, l.moduleWalkerV0_10_7V0_10_8())
   115  	if !diags.HasErrors() {
   116  		return cfg, nil
   117  	}
   118  	log.Print("[DEBUG] Failed to load modules using the v0.10.7 ~ v0.10.8 module walker; Trying the v0.11.0 ~ v0.11.7 module walker...")
   119  	log.Printf("[DEBUG] Original error: %s", diags)
   120  
   121  	cfg, diags = configs.BuildConfig(rootMod, l.moduleWalkerV0_11_0V0_11_7())
   122  	if !diags.HasErrors() {
   123  		return cfg, nil
   124  	}
   125  	log.Printf("[ERROR] Failed to load modules using the v0.11.0 ~ v0.11.7 module walker; Trying the v0.12 module walker...")
   126  	log.Printf("[DEBUG] Original error: %s", diags)
   127  
   128  	cfg, diags = configs.BuildConfig(rootMod, l.moduleWalkerV0_12())
   129  	if !diags.HasErrors() {
   130  		return cfg, nil
   131  	}
   132  
   133  	log.Printf("[ERROR] Failed to load modules using the v0.12 module walker: %s", diags)
   134  	return nil, diags
   135  }
   136  
   137  // LoadValuesFiles reads Terraform's values files and returns terraform.InputValues list in order of priority
   138  // Pass values ​​files specified from the CLI as the arguments in order of priority
   139  // This is the responsibility of the caller
   140  func (l *Loader) LoadValuesFiles(files ...string) ([]terraform.InputValues, error) {
   141  	log.Print("[INFO] Load values files")
   142  
   143  	values := []terraform.InputValues{}
   144  
   145  	for _, file := range files {
   146  		if _, err := os.Stat(file); os.IsNotExist(err) {
   147  			return values, fmt.Errorf("`%s` is not found", file)
   148  		}
   149  	}
   150  
   151  	autoLoadFiles, err := autoLoadValuesFiles()
   152  	if err != nil {
   153  		log.Printf("[ERROR] %s", err)
   154  		return nil, err
   155  	}
   156  	if _, err := os.Stat(defaultValuesFile); !os.IsNotExist(err) {
   157  		autoLoadFiles = append([]string{defaultValuesFile}, autoLoadFiles...)
   158  	}
   159  
   160  	for _, file := range autoLoadFiles {
   161  		vals, err := l.loadValuesFile(file, terraform.ValueFromAutoFile)
   162  		if err != nil {
   163  			return nil, err
   164  		}
   165  		values = append(values, vals)
   166  	}
   167  	for _, file := range files {
   168  		vals, err := l.loadValuesFile(file, terraform.ValueFromNamedFile)
   169  		if err != nil {
   170  			return nil, err
   171  		}
   172  		values = append(values, vals)
   173  	}
   174  
   175  	return values, nil
   176  }
   177  
   178  // IsConfigFile checks whether the configuration files includes the file
   179  func (l *Loader) IsConfigFile(file string) bool {
   180  	for _, configFile := range l.configFiles {
   181  		if file == configFile {
   182  			return true
   183  		}
   184  	}
   185  	return false
   186  }
   187  
   188  // autoLoadValuesFiles returns all files which match *.auto.tfvars present in the current directory
   189  // The list is sorted alphabetically. This is equivalent to priority
   190  // Please note that terraform.tfvars is not included in this list
   191  func autoLoadValuesFiles() ([]string, error) {
   192  	files, err := ioutil.ReadDir(".")
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  
   197  	ret := []string{}
   198  	for _, file := range files {
   199  		if file.IsDir() {
   200  			continue
   201  		}
   202  
   203  		if strings.HasSuffix(file.Name(), ".auto.tfvars") || strings.HasSuffix(file.Name(), ".auto.tfvars.json") {
   204  			ret = append(ret, file.Name())
   205  		}
   206  	}
   207  	sort.Strings(ret)
   208  
   209  	return ret, nil
   210  }
   211  
   212  func (l *Loader) loadValuesFile(file string, sourceType terraform.ValueSourceType) (terraform.InputValues, error) {
   213  	log.Printf("[INFO] Load `%s`", file)
   214  	vals, diags := l.loader.Parser().LoadValuesFile(file)
   215  	if diags.HasErrors() {
   216  		log.Printf("[ERROR] %s", diags)
   217  		if diags[0].Subject == nil {
   218  			// HACK: When Subject is nil, it outputs unintended message, so it replaces with actual file.
   219  			return nil, errors.New(strings.Replace(diags.Error(), "<nil>: ", fmt.Sprintf("%s: ", file), 1))
   220  		}
   221  		return nil, diags
   222  	}
   223  
   224  	ret := make(terraform.InputValues)
   225  	for k, v := range vals {
   226  		ret[k] = &terraform.InputValue{
   227  			Value:      v,
   228  			SourceType: sourceType,
   229  		}
   230  	}
   231  	return ret, nil
   232  }
   233  
   234  func (l *Loader) moduleWalkerLegacy() configs.ModuleWalker {
   235  	return configs.ModuleWalkerFunc(func(req *configs.ModuleRequest) (*configs.Module, *version.Version, hcl.Diagnostics) {
   236  		key := "root." + req.Name + "-" + req.SourceAddr
   237  		dir := makeModuleDirFromKey(key)
   238  		log.Printf("[DEBUG] Trying to load the module: key=%s, dir=%s", key, dir)
   239  		mod, diags := l.loader.Parser().LoadConfigDir(dir)
   240  		return mod, nil, diags
   241  	})
   242  }
   243  
   244  func (l *Loader) moduleWalkerV0_10_6() configs.ModuleWalker {
   245  	return configs.ModuleWalkerFunc(func(req *configs.ModuleRequest) (*configs.Module, *version.Version, hcl.Diagnostics) {
   246  		key := "module." + req.Name + "-" + req.SourceAddr
   247  		dir := makeModuleDirFromKey(key)
   248  		log.Printf("[DEBUG] Trying to load the module: key=%s, dir=%s", key, dir)
   249  		mod, diags := l.loader.Parser().LoadConfigDir(dir)
   250  		return mod, nil, diags
   251  	})
   252  }
   253  
   254  func (l *Loader) moduleWalkerV0_10_7V0_10_8() configs.ModuleWalker {
   255  	return configs.ModuleWalkerFunc(func(req *configs.ModuleRequest) (*configs.Module, *version.Version, hcl.Diagnostics) {
   256  		key := "0.root." + req.Name + "-" + req.SourceAddr
   257  		dir := makeModuleDirFromKey(key)
   258  		log.Printf("[DEBUG] Trying to load the module: key=%s, dir=%s", key, dir)
   259  		mod, diags := l.loader.Parser().LoadConfigDir(dir)
   260  		return mod, nil, diags
   261  	})
   262  }
   263  
   264  func (l *Loader) moduleWalkerV0_11_0V0_11_7() configs.ModuleWalker {
   265  	return configs.ModuleWalkerFunc(func(req *configs.ModuleRequest) (*configs.Module, *version.Version, hcl.Diagnostics) {
   266  		path := append(buildParentModulePathTree([]string{}, req.Parent), l.getModulePath(req))
   267  		key := "1." + strings.Join(path, "|")
   268  
   269  		record, ok := l.moduleManifest[key]
   270  		if !ok {
   271  			log.Printf("[DEBUG] Failed to search by `%s` key.", key)
   272  			return nil, nil, hcl.Diagnostics{
   273  				{
   274  					Severity: hcl.DiagError,
   275  					Summary:  fmt.Sprintf("`%s` module is not found. Did you run `terraform init`?", req.Name),
   276  					Subject:  &req.CallRange,
   277  				},
   278  			}
   279  		}
   280  
   281  		dir := record.Dir
   282  		if record.Root != "" {
   283  			dir = filepath.Join(dir, record.Root)
   284  		}
   285  		log.Printf("[DEBUG] Trying to load the module: key=%s, version=%s, dir=%s", key, record.VersionStr, dir)
   286  
   287  		mod, diags := l.loader.Parser().LoadConfigDir(dir)
   288  		return mod, record.Version, diags
   289  	})
   290  }
   291  
   292  func (l *Loader) moduleWalkerV0_12() configs.ModuleWalker {
   293  	return configs.ModuleWalkerFunc(func(req *configs.ModuleRequest) (*configs.Module, *version.Version, hcl.Diagnostics) {
   294  		key := req.Path.String()
   295  		record, ok := l.moduleManifest[key]
   296  		if !ok {
   297  			log.Printf("[DEBUG] Failed to search by `%s` key.", key)
   298  			return nil, nil, hcl.Diagnostics{
   299  				{
   300  					Severity: hcl.DiagError,
   301  					Summary:  fmt.Sprintf("`%s` module is not found. Did you run `terraform init`?", req.Name),
   302  					Subject:  &req.CallRange,
   303  				},
   304  			}
   305  		}
   306  
   307  		dir := record.Dir
   308  		if record.Root != "" {
   309  			dir = filepath.Join(dir, record.Root)
   310  		}
   311  		log.Printf("[DEBUG] Trying to load the module: key=%s, version=%s, dir=%s", key, record.VersionStr, dir)
   312  
   313  		mod, diags := l.loader.Parser().LoadConfigDir(dir)
   314  		return mod, record.Version, diags
   315  	})
   316  }
   317  
   318  func (l *Loader) initializeModuleManifest() error {
   319  	file, err := ioutil.ReadFile(getTFModuleManifestPath())
   320  	if err != nil {
   321  		return err
   322  	}
   323  	log.Printf("[DEBUG] Parsing the module manifest file: %s", file)
   324  
   325  	var manifestFile moduleManifestFile
   326  	err = json.Unmarshal(file, &manifestFile)
   327  	if err != nil {
   328  		return err
   329  	}
   330  
   331  	for _, m := range manifestFile.Modules {
   332  		if m.VersionStr != "" {
   333  			m.Version, err = version.NewVersion(m.VersionStr)
   334  			if err != nil {
   335  				return err
   336  			}
   337  			l.moduleSourceVersions[m.Source] = append(l.moduleSourceVersions[m.Source], m.Version)
   338  		}
   339  		l.moduleManifest[m.Key] = m
   340  	}
   341  
   342  	return nil
   343  }
   344  
   345  func makeModuleDirFromKey(key string) string {
   346  	sum := md5.Sum([]byte(key))
   347  	return filepath.Join(getTFModuleDir(), hex.EncodeToString(sum[:]))
   348  }
   349  
   350  func buildParentModulePathTree(path []string, cfg *configs.Config) []string {
   351  	if cfg.Path.IsRoot() {
   352  		// @see https://github.com/golang/go/wiki/SliceTricks#reversing
   353  		for i := len(path)/2 - 1; i >= 0; i-- {
   354  			opp := len(path) - 1 - i
   355  			path[i], path[opp] = path[opp], path[i]
   356  		}
   357  		return path
   358  	}
   359  
   360  	_, call := cfg.Path.Call()
   361  	key := call.Name
   362  	if cfg.Version != nil {
   363  		key += "#" + cfg.Version.String()
   364  	}
   365  	key += ";" + cfg.SourceAddr
   366  	path = append(path, key)
   367  
   368  	return buildParentModulePathTree(path, cfg.Parent)
   369  }
   370  
   371  func (l *Loader) getModulePath(req *configs.ModuleRequest) string {
   372  	key := req.Name + ";" + req.SourceAddr
   373  	if len(req.VersionConstraint.Required) > 0 {
   374  		log.Printf("[DEBUG] Processing the `%s` module: constraints=%#v", req.Name, req.VersionConstraint)
   375  		sourceVersions := l.moduleSourceVersions[req.SourceAddr]
   376  
   377  		var latest *version.Version
   378  		for _, v := range sourceVersions {
   379  			if req.VersionConstraint.Required.Check(v) {
   380  				if latest == nil || v.GreaterThan(latest) {
   381  					latest = v
   382  				}
   383  			} else {
   384  				log.Printf("[INFO] `%s` doesn't satisfy the version constraint. Ignored.", v)
   385  			}
   386  		}
   387  
   388  		if latest == nil {
   389  			panic(fmt.Errorf("There is no version that satisfies the constraints: name=%s, constraints=%#v, versions=%#v", req.Name, req.VersionConstraint, l.moduleSourceVersions[req.SourceAddr]))
   390  		}
   391  		key += "." + latest.String()
   392  	}
   393  
   394  	return key
   395  }