github.com/jpreese/tflint@v0.19.2-0.20200908152133-b01686250fb6/tflint/config.go (about) 1 package tflint 2 3 import ( 4 "errors" 5 "fmt" 6 "log" 7 "os" 8 9 hcl "github.com/hashicorp/hcl/v2" 10 "github.com/hashicorp/hcl/v2/gohcl" 11 "github.com/hashicorp/hcl/v2/hclparse" 12 homedir "github.com/mitchellh/go-homedir" 13 tfplugin "github.com/terraform-linters/tflint-plugin-sdk/tflint" 14 "github.com/terraform-linters/tflint/client" 15 ) 16 17 var defaultConfigFile = ".tflint.hcl" 18 var fallbackConfigFile = "~/.tflint.hcl" 19 20 var removedRulesMap = map[string]string{ 21 "terraform_dash_in_data_source_name": "`terraform_dash_in_data_source_name` rule was removed in v0.16.0. Please use `terraform_naming_convention` rule instead", 22 "terraform_dash_in_module_name": "`terraform_dash_in_module_name` rule was removed in v0.16.0. Please use `terraform_naming_convention` rule instead", 23 "terraform_dash_in_output_name": "`terraform_dash_in_output_name` rule was removed in v0.16.0. Please use `terraform_naming_convention` rule instead", 24 "terraform_dash_in_resource_name": "`terraform_dash_in_resource_name` rule was removed in v0.16.0. Please use `terraform_naming_convention` rule instead", 25 } 26 27 type rawConfig struct { 28 Config *struct { 29 Module *bool `hcl:"module"` 30 DeepCheck *bool `hcl:"deep_check"` 31 Force *bool `hcl:"force"` 32 AwsCredentials *map[string]string `hcl:"aws_credentials"` 33 IgnoreModule *map[string]bool `hcl:"ignore_module"` 34 Varfile *[]string `hcl:"varfile"` 35 Variables *[]string `hcl:"variables"` 36 DisabledByDefault *bool `hcl:"disabled_by_default"` 37 // Removed options 38 TerraformVersion *string `hcl:"terraform_version"` 39 IgnoreRule *map[string]bool `hcl:"ignore_rule"` 40 } `hcl:"config,block"` 41 Rules []RuleConfig `hcl:"rule,block"` 42 Plugins []PluginConfig `hcl:"plugin,block"` 43 } 44 45 // Config describes the behavior of TFLint 46 type Config struct { 47 Module bool 48 DeepCheck bool 49 Force bool 50 AwsCredentials client.AwsCredentials 51 IgnoreModules map[string]bool 52 Varfiles []string 53 Variables []string 54 DisabledByDefault bool 55 Rules map[string]*RuleConfig 56 Plugins map[string]*PluginConfig 57 } 58 59 // RuleConfig is a TFLint's rule config 60 type RuleConfig struct { 61 Name string `hcl:"name,label"` 62 Enabled bool `hcl:"enabled"` 63 Body hcl.Body `hcl:",remain"` 64 } 65 66 // PluginConfig is a TFLint's plugin config 67 type PluginConfig struct { 68 Name string `hcl:"name,label"` 69 Enabled bool `hcl:"enabled"` 70 } 71 72 // EmptyConfig returns default config 73 // It is mainly used for testing 74 func EmptyConfig() *Config { 75 return &Config{ 76 Module: false, 77 DeepCheck: false, 78 Force: false, 79 AwsCredentials: client.AwsCredentials{}, 80 IgnoreModules: map[string]bool{}, 81 Varfiles: []string{}, 82 Variables: []string{}, 83 DisabledByDefault: false, 84 Rules: map[string]*RuleConfig{}, 85 Plugins: map[string]*PluginConfig{}, 86 } 87 } 88 89 // LoadConfig loads TFLint config from file 90 // If failed to load the default config file, it tries to load config file under the home directory 91 // Therefore, if there is no default config file, it will not return an error 92 func LoadConfig(file string) (*Config, error) { 93 log.Printf("[INFO] Load config: %s", file) 94 if _, err := os.Stat(file); !os.IsNotExist(err) { 95 cfg, err := loadConfigFromFile(file) 96 if err != nil { 97 log.Printf("[ERROR] %s", err) 98 return nil, err 99 } 100 return cfg, nil 101 } else if file != defaultConfigFile { 102 log.Printf("[ERROR] %s", err) 103 return nil, fmt.Errorf("`%s` is not found", file) 104 } else { 105 log.Printf("[INFO] Default config file is not found. Ignored") 106 } 107 108 fallback, err := homedir.Expand(fallbackConfigFile) 109 if err != nil { 110 log.Printf("[ERROR] %s", err) 111 return nil, err 112 } 113 114 log.Printf("[INFO] Load fallback config: %s", fallback) 115 if _, err := os.Stat(fallback); !os.IsNotExist(err) { 116 cfg, err := loadConfigFromFile(fallback) 117 if err != nil { 118 return nil, err 119 } 120 return cfg, nil 121 } 122 log.Printf("[INFO] Fallback config file is not found. Ignored") 123 124 log.Print("[INFO] Use default config") 125 return EmptyConfig(), nil 126 } 127 128 // Merge returns a merged copy of the two configs 129 // Since the argument takes precedence, it can be used as overwriting of the config 130 func (c *Config) Merge(other *Config) *Config { 131 ret := c.copy() 132 133 if other.Module { 134 ret.Module = true 135 } 136 if other.DeepCheck { 137 ret.DeepCheck = true 138 } 139 if other.Force { 140 ret.Force = true 141 } 142 if other.DisabledByDefault { 143 ret.DisabledByDefault = true 144 } 145 146 ret.AwsCredentials = ret.AwsCredentials.Merge(other.AwsCredentials) 147 ret.IgnoreModules = mergeBoolMap(ret.IgnoreModules, other.IgnoreModules) 148 ret.Varfiles = append(ret.Varfiles, other.Varfiles...) 149 ret.Variables = append(ret.Variables, other.Variables...) 150 151 ret.Rules = mergeRuleMap(ret.Rules, other.Rules, other.DisabledByDefault) 152 ret.Plugins = mergePluginMap(ret.Plugins, other.Plugins) 153 154 return ret 155 } 156 157 // ToPluginConfig converts self into the plugin configuration format 158 func (c *Config) ToPluginConfig() *tfplugin.Config { 159 cfg := &tfplugin.Config{ 160 Rules: map[string]*tfplugin.RuleConfig{}, 161 DisabledByDefault: c.DisabledByDefault, 162 } 163 for _, rule := range c.Rules { 164 cfg.Rules[rule.Name] = &tfplugin.RuleConfig{ 165 Name: rule.Name, 166 Enabled: rule.Enabled, 167 } 168 } 169 return cfg 170 } 171 172 // RuleSet is an interface to handle plugin's RuleSet and core RuleSet both 173 // In the future, when all RuleSets are cut out into plugins, it will no longer be needed. 174 type RuleSet interface { 175 RuleSetName() (string, error) 176 RuleSetVersion() (string, error) 177 RuleNames() ([]string, error) 178 } 179 180 // ValidateRules checks for duplicate rule names, for invalid rule names, and so on. 181 func (c *Config) ValidateRules(rulesets ...RuleSet) error { 182 rulesMap := map[string]string{} 183 for _, ruleset := range rulesets { 184 ruleNames, err := ruleset.RuleNames() 185 if err != nil { 186 return err 187 } 188 189 for _, rule := range ruleNames { 190 rulesetName, err := ruleset.RuleSetName() 191 if err != nil { 192 return err 193 } 194 195 if existsName, exists := rulesMap[rule]; exists { 196 return fmt.Errorf("`%s` is duplicated in %s and %s", rule, existsName, rulesetName) 197 } 198 rulesMap[rule] = rulesetName 199 } 200 } 201 202 for _, rule := range c.Rules { 203 if _, exists := rulesMap[rule.Name]; !exists { 204 if message, exists := removedRulesMap[rule.Name]; exists { 205 return errors.New(message) 206 } 207 return fmt.Errorf("Rule not found: %s", rule.Name) 208 } 209 } 210 211 return nil 212 } 213 214 func (c *Config) copy() *Config { 215 ignoreModules := make(map[string]bool) 216 for k, v := range c.IgnoreModules { 217 ignoreModules[k] = v 218 } 219 220 varfiles := make([]string, len(c.Varfiles)) 221 copy(varfiles, c.Varfiles) 222 223 variables := make([]string, len(c.Variables)) 224 copy(variables, c.Variables) 225 226 rules := map[string]*RuleConfig{} 227 for k, v := range c.Rules { 228 rules[k] = &RuleConfig{} 229 *rules[k] = *v 230 } 231 232 plugins := map[string]*PluginConfig{} 233 for k, v := range c.Plugins { 234 plugins[k] = &PluginConfig{} 235 *plugins[k] = *v 236 } 237 238 return &Config{ 239 Module: c.Module, 240 DeepCheck: c.DeepCheck, 241 Force: c.Force, 242 AwsCredentials: c.AwsCredentials, 243 IgnoreModules: ignoreModules, 244 Varfiles: varfiles, 245 Variables: variables, 246 DisabledByDefault: c.DisabledByDefault, 247 Rules: rules, 248 Plugins: plugins, 249 } 250 } 251 252 func loadConfigFromFile(file string) (*Config, error) { 253 parser := hclparse.NewParser() 254 255 f, diags := parser.ParseHCLFile(file) 256 if diags.HasErrors() { 257 return nil, diags 258 } 259 260 var raw rawConfig 261 diags = gohcl.DecodeBody(f.Body, nil, &raw) 262 if diags.HasErrors() { 263 return nil, diags 264 } 265 266 if raw.Config != nil { 267 if raw.Config.TerraformVersion != nil { 268 return nil, errors.New("`terraform_version` was removed in v0.9.0 because the option is no longer used") 269 } 270 271 if raw.Config.IgnoreRule != nil { 272 return nil, errors.New("`ignore_rule` was removed in v0.12.0. Please define `rule` block with `enabled = false` instead") 273 } 274 } 275 276 cfg := raw.toConfig() 277 log.Printf("[DEBUG] Config loaded") 278 log.Printf("[DEBUG] Module: %t", cfg.Module) 279 log.Printf("[DEBUG] DeepCheck: %t", cfg.DeepCheck) 280 log.Printf("[DEBUG] Force: %t", cfg.Force) 281 log.Printf("[DEBUG] IgnoreModules: %#v", cfg.IgnoreModules) 282 log.Printf("[DEBUG] Varfiles: %#v", cfg.Varfiles) 283 log.Printf("[DEBUG] Variables: %#v", cfg.Variables) 284 log.Printf("[DEBUG] DisabledByDefault: %#v", cfg.DisabledByDefault) 285 log.Printf("[DEBUG] Rules: %#v", cfg.Rules) 286 log.Printf("[DEBUG] Plugins: %#v", cfg.Plugins) 287 288 return cfg, nil 289 } 290 291 func mergeBoolMap(a, b map[string]bool) map[string]bool { 292 ret := map[string]bool{} 293 for k, v := range a { 294 ret[k] = v 295 } 296 for k, v := range b { 297 ret[k] = v 298 } 299 return ret 300 } 301 302 func mergeRuleMap(a, b map[string]*RuleConfig, bDisabledByDefault bool) map[string]*RuleConfig { 303 ret := map[string]*RuleConfig{} 304 if bDisabledByDefault { 305 for bK, bV := range b { 306 configRuleFound := false 307 for aK, aV := range a { 308 if aK == bK { 309 ret[bK] = bV 310 ret[bK].Body = aV.Body 311 ret[bK].Enabled = true 312 configRuleFound = true 313 } 314 } 315 if !configRuleFound { 316 ret[bK] = bV 317 ret[bK].Enabled = true 318 } 319 } 320 return ret 321 } 322 323 for k, v := range a { 324 ret[k] = v 325 } 326 for k, v := range b { 327 // HACK: If you enable the rule through the CLI instead of the file, its hcl.Body will not contain valid range. 328 // @see https://github.com/hashicorp/hcl/blob/v2.5.0/merged.go#L132-L135 329 if prevConfig, exists := ret[k]; exists && v.Body.MissingItemRange().Filename == "<empty>" { 330 ret[k] = v 331 // Do not override body 332 ret[k].Body = prevConfig.Body 333 } else { 334 ret[k] = v 335 } 336 } 337 return ret 338 } 339 340 func mergePluginMap(a, b map[string]*PluginConfig) map[string]*PluginConfig { 341 ret := map[string]*PluginConfig{} 342 for k, v := range a { 343 ret[k] = v 344 } 345 for k, v := range b { 346 ret[k] = v 347 } 348 return ret 349 } 350 351 func (raw *rawConfig) toConfig() *Config { 352 ret := EmptyConfig() 353 rc := raw.Config 354 355 if rc != nil { 356 if rc.Module != nil { 357 ret.Module = *rc.Module 358 } 359 if rc.DeepCheck != nil { 360 ret.DeepCheck = *rc.DeepCheck 361 } 362 if rc.Force != nil { 363 ret.Force = *rc.Force 364 } 365 if rc.DisabledByDefault != nil { 366 ret.DisabledByDefault = *rc.DisabledByDefault 367 } 368 if rc.AwsCredentials != nil { 369 credentials := *rc.AwsCredentials 370 ret.AwsCredentials.AccessKey = credentials["access_key"] 371 ret.AwsCredentials.SecretKey = credentials["secret_key"] 372 ret.AwsCredentials.Profile = credentials["profile"] 373 ret.AwsCredentials.CredsFile = credentials["shared_credentials_file"] 374 ret.AwsCredentials.Region = credentials["region"] 375 } 376 if rc.IgnoreModule != nil { 377 ret.IgnoreModules = *rc.IgnoreModule 378 } 379 if rc.Varfile != nil { 380 ret.Varfiles = *rc.Varfile 381 } 382 if rc.Variables != nil { 383 ret.Variables = *rc.Variables 384 } 385 } 386 387 for _, r := range raw.Rules { 388 var rule = r 389 ret.Rules[rule.Name] = &rule 390 } 391 392 for _, p := range raw.Plugins { 393 var plugin = p 394 ret.Plugins[plugin.Name] = &plugin 395 } 396 397 return ret 398 }