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