github.com/tetrafolium/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 }