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 }