github.com/tetrafolium/tflint@v0.8.0/tflint/runner.go (about) 1 package tflint 2 3 import ( 4 "errors" 5 "fmt" 6 "log" 7 "path/filepath" 8 "strings" 9 "sync" 10 11 "github.com/hashicorp/hcl2/hcl" 12 "github.com/hashicorp/terraform/addrs" 13 "github.com/hashicorp/terraform/configs" 14 "github.com/hashicorp/terraform/lang" 15 "github.com/hashicorp/terraform/terraform" 16 "github.com/wata727/tflint/client" 17 "github.com/wata727/tflint/issue" 18 "github.com/zclconf/go-cty/cty" 19 "github.com/zclconf/go-cty/cty/convert" 20 "github.com/zclconf/go-cty/cty/gocty" 21 ) 22 23 // Runner checks templates according rules. 24 // For variables interplation, it has Terraform eval context. 25 // After checking, it accumulates results as issues. 26 type Runner struct { 27 TFConfig *configs.Config 28 Issues issue.Issues 29 AwsClient *client.AwsClient 30 31 ctx terraform.BuiltinEvalContext 32 config *Config 33 } 34 35 // Rule is interface for building the issue 36 type Rule interface { 37 Name() string 38 Type() string 39 Link() string 40 } 41 42 // NewRunner returns new TFLint runner 43 // It prepares built-in context (workpace metadata, variables) from 44 // received `configs.Config` and `terraform.InputValues` 45 func NewRunner(c *Config, cfg *configs.Config, variables ...terraform.InputValues) *Runner { 46 path := "root" 47 if !cfg.Path.IsRoot() { 48 path = cfg.Path.String() 49 } 50 log.Printf("[INFO] Initialize new runner for %s", path) 51 52 return &Runner{ 53 TFConfig: cfg, 54 Issues: []*issue.Issue{}, 55 AwsClient: client.NewAwsClient(c.AwsCredentials), 56 57 ctx: terraform.BuiltinEvalContext{ 58 Evaluator: &terraform.Evaluator{ 59 Meta: &terraform.ContextMeta{ 60 Env: getTFWorkspace(), 61 }, 62 Config: cfg, 63 VariableValues: prepareVariableValues(cfg.Module.Variables, variables...), 64 VariableValuesLock: &sync.Mutex{}, 65 }, 66 }, 67 config: c, 68 } 69 } 70 71 // NewModuleRunners returns new TFLint runners for child modules 72 // Recursively search modules and generate Runners 73 // In order to propagate attributes of moduleCall as variables to the module, 74 // evaluate the variables. If it cannot be evaluated, treat it as unknown 75 func NewModuleRunners(parent *Runner) ([]*Runner, error) { 76 runners := []*Runner{} 77 78 for name, cfg := range parent.TFConfig.Children { 79 moduleCall, ok := parent.TFConfig.Module.ModuleCalls[name] 80 if !ok { 81 panic(fmt.Errorf("Expected module call `%s` is not found in `%s`", name, parent.TFConfig.Path.String())) 82 } 83 if parent.TFConfig.Path.IsRoot() && parent.config.IgnoreModule[moduleCall.SourceAddr] { 84 log.Printf("[INFO] Ignore `%s` module", moduleCall.Name) 85 continue 86 } 87 88 attributes, diags := moduleCall.Config.JustAttributes() 89 if diags.HasErrors() { 90 var causeErr error 91 if diags[0].Subject == nil { 92 // HACK: When Subject is nil, it outputs unintended message, so it replaces with actual file. 93 causeErr = errors.New(strings.Replace(diags.Error(), "<nil>: ", "", 1)) 94 } else { 95 causeErr = diags 96 } 97 err := &Error{ 98 Code: UnexpectedAttributeError, 99 Level: ErrorLevel, 100 Message: fmt.Sprintf( 101 "Attribute of module not allowed was found in %s:%d", 102 parent.getFileName(moduleCall.DeclRange.Filename), 103 moduleCall.DeclRange.Start.Line, 104 ), 105 Cause: causeErr, 106 } 107 log.Printf("[ERROR] %s", err) 108 return runners, err 109 } 110 111 for varName, rawVar := range cfg.Module.Variables { 112 if attribute, exists := attributes[varName]; exists { 113 if isEvaluable(attribute.Expr) { 114 val, diags := parent.ctx.EvaluateExpr(attribute.Expr, cty.DynamicPseudoType, nil) 115 if diags.HasErrors() { 116 err := &Error{ 117 Code: EvaluationError, 118 Level: ErrorLevel, 119 Message: fmt.Sprintf( 120 "Failed to eval an expression in %s:%d", 121 parent.getFileName(attribute.Expr.Range().Filename), 122 attribute.Expr.Range().Start.Line, 123 ), 124 Cause: diags.Err(), 125 } 126 log.Printf("[ERROR] %s", err) 127 return runners, err 128 } 129 rawVar.Default = val 130 } else { 131 // If module attributes are not evaluable, it marks that value as unknown. 132 // Unknown values are ignored when evaluated inside the module. 133 log.Printf("[DEBUG] `%s` has been marked as unknown", varName) 134 rawVar.Default = cty.UnknownVal(cty.DynamicPseudoType) 135 } 136 } 137 } 138 139 runner := NewRunner(parent.config, cfg) 140 runners = append(runners, runner) 141 moudleRunners, err := NewModuleRunners(runner) 142 if err != nil { 143 return runners, err 144 } 145 runners = append(runners, moudleRunners...) 146 } 147 148 return runners, nil 149 } 150 151 // EvaluateExpr is a wrapper of terraform.BultinEvalContext.EvaluateExpr and gocty.FromCtyValue 152 // When it received slice as `ret`, it converts cty.Value to expected list type 153 // because raw cty.Value has TupleType. 154 func (r *Runner) EvaluateExpr(expr hcl.Expression, ret interface{}) error { 155 if !isEvaluable(expr) { 156 err := &Error{ 157 Code: UnevaluableError, 158 Level: WarningLevel, 159 Message: fmt.Sprintf( 160 "Unevaluable expression found in %s:%d", 161 r.getFileName(expr.Range().Filename), 162 expr.Range().Start.Line, 163 ), 164 } 165 log.Printf("[WARN] %s; TFLint ignores an unevaluable expression.", err) 166 return err 167 } 168 169 val, diags := r.ctx.EvaluateExpr(expr, cty.DynamicPseudoType, nil) 170 if diags.HasErrors() { 171 err := &Error{ 172 Code: EvaluationError, 173 Level: ErrorLevel, 174 Message: fmt.Sprintf( 175 "Failed to eval an expression in %s:%d", 176 r.getFileName(expr.Range().Filename), 177 expr.Range().Start.Line, 178 ), 179 Cause: diags.Err(), 180 } 181 log.Printf("[ERROR] %s", err) 182 return err 183 } 184 185 err := cty.Walk(val, func(path cty.Path, v cty.Value) (bool, error) { 186 if !v.IsKnown() { 187 err := &Error{ 188 Code: UnknownValueError, 189 Level: WarningLevel, 190 Message: fmt.Sprintf( 191 "Unknown value found in %s:%d; Please use environment variables or tfvars to set the value", 192 r.getFileName(expr.Range().Filename), 193 expr.Range().Start.Line, 194 ), 195 } 196 log.Printf("[WARN] %s; TFLint ignores an expression includes an unknown value.", err) 197 return false, err 198 } 199 200 if v.IsNull() { 201 err := &Error{ 202 Code: NullValueError, 203 Level: WarningLevel, 204 Message: fmt.Sprintf( 205 "Null value found in %s:%d", 206 r.getFileName(expr.Range().Filename), 207 expr.Range().Start.Line, 208 ), 209 } 210 log.Printf("[WARN] %s; TFLint ignores an expression includes an null value.", err) 211 return false, err 212 } 213 214 return true, nil 215 }) 216 217 if err != nil { 218 return err 219 } 220 221 switch ret.(type) { 222 case *string: 223 val, err = convert.Convert(val, cty.String) 224 case *int: 225 val, err = convert.Convert(val, cty.Number) 226 case *[]string: 227 val, err = convert.Convert(val, cty.List(cty.String)) 228 case *[]int: 229 val, err = convert.Convert(val, cty.List(cty.Number)) 230 case *map[string]string: 231 val, err = convert.Convert(val, cty.Map(cty.String)) 232 case *map[string]int: 233 val, err = convert.Convert(val, cty.Map(cty.Number)) 234 } 235 236 if err != nil { 237 err := &Error{ 238 Code: TypeConversionError, 239 Level: ErrorLevel, 240 Message: fmt.Sprintf( 241 "Invalid type expression in %s:%d", 242 r.getFileName(expr.Range().Filename), 243 expr.Range().Start.Line, 244 ), 245 Cause: err, 246 } 247 log.Printf("[ERROR] %s", err) 248 return err 249 } 250 251 err = gocty.FromCtyValue(val, ret) 252 if err != nil { 253 err := &Error{ 254 Code: TypeMismatchError, 255 Level: ErrorLevel, 256 Message: fmt.Sprintf( 257 "Invalid type expression in %s:%d", 258 r.getFileName(expr.Range().Filename), 259 expr.Range().Start.Line, 260 ), 261 Cause: err, 262 } 263 log.Printf("[ERROR] %s", err) 264 return err 265 } 266 return nil 267 } 268 269 // TFConfigPath is a wrapper of addrs.Module 270 func (r *Runner) TFConfigPath() string { 271 if r.TFConfig.Path.IsRoot() { 272 return "root" 273 } 274 return r.TFConfig.Path.String() 275 } 276 277 // LookupIssues returns issues according to the received files 278 func (r *Runner) LookupIssues(files ...string) issue.Issues { 279 if len(files) == 0 { 280 return r.Issues 281 } 282 283 issues := []*issue.Issue{} 284 for _, issue := range r.Issues { 285 for _, file := range files { 286 if file == issue.File { 287 issues = append(issues, issue) 288 } 289 } 290 } 291 return issues 292 } 293 294 // WalkResourceAttributes searches for resources and passes the appropriate attributes to the walker function 295 func (r *Runner) WalkResourceAttributes(resource, attributeName string, walker func(*hcl.Attribute) error) error { 296 for _, resource := range r.LookupResourcesByType(resource) { 297 body, _, diags := resource.Config.PartialContent(&hcl.BodySchema{ 298 Attributes: []hcl.AttributeSchema{ 299 { 300 Name: attributeName, 301 }, 302 }, 303 }) 304 if diags.HasErrors() { 305 return diags 306 } 307 308 if attribute, ok := body.Attributes[attributeName]; ok { 309 log.Printf("[DEBUG] Walk `%s` attribute", resource.Type+"."+resource.Name+"."+attributeName) 310 err := walker(attribute) 311 if err != nil { 312 return err 313 } 314 } 315 } 316 317 return nil 318 } 319 320 // WalkResourceBlocks walks all blocks of the passed resource and invokes the passed function 321 func (r *Runner) WalkResourceBlocks(resource, blockType string, walker func(*hcl.Block) error) error { 322 for _, resource := range r.LookupResourcesByType(resource) { 323 body, _, diags := resource.Config.PartialContent(&hcl.BodySchema{ 324 Blocks: []hcl.BlockHeaderSchema{ 325 { 326 Type: blockType, 327 }, 328 }, 329 }) 330 if diags.HasErrors() { 331 return diags 332 } 333 334 for _, block := range body.Blocks { 335 log.Printf("[DEBUG] Walk `%s` block", resource.Type+"."+resource.Name+"."+blockType) 336 err := walker(block) 337 if err != nil { 338 return err 339 } 340 } 341 } 342 343 return nil 344 } 345 346 // EnsureNoError is a helper for processing when no error occurs 347 // This function skips processing without returning an error to the caller when the error is warning 348 func (r *Runner) EnsureNoError(err error, proc func() error) error { 349 if err == nil { 350 return proc() 351 } 352 353 if appErr, ok := err.(*Error); ok { 354 switch appErr.Level { 355 case WarningLevel: 356 return nil 357 case ErrorLevel: 358 return appErr 359 default: 360 panic(appErr) 361 } 362 } else { 363 return err 364 } 365 } 366 367 // IsNullExpr check the passed expression is null 368 func (r *Runner) IsNullExpr(expr hcl.Expression) bool { 369 val, _ := r.ctx.EvaluateExpr(expr, cty.DynamicPseudoType, nil) 370 return val.IsNull() 371 } 372 373 // LookupResourcesByType returns `configs.Resource` list according to the resource type 374 func (r *Runner) LookupResourcesByType(resourceType string) []*configs.Resource { 375 ret := []*configs.Resource{} 376 377 for _, resource := range r.TFConfig.Module.ManagedResources { 378 if resource.Type == resourceType { 379 ret = append(ret, resource) 380 } 381 } 382 383 return ret 384 } 385 386 // EachStringSliceExprs iterates an evaluated value and the corresponding expression 387 // If the given expression is a static list, get an expression for each value 388 // If not, the given expression is used as it is 389 func (r *Runner) EachStringSliceExprs(expr hcl.Expression, proc func(val string, expr hcl.Expression)) error { 390 var vals []string 391 err := r.EvaluateExpr(expr, &vals) 392 393 exprs, diags := hcl.ExprList(expr) 394 if diags.HasErrors() { 395 log.Printf("[DEBUG] Expr is not static list: %s", diags) 396 for range vals { 397 exprs = append(exprs, expr) 398 } 399 } 400 401 return r.EnsureNoError(err, func() error { 402 for idx, val := range vals { 403 proc(val, exprs[idx]) 404 } 405 return nil 406 }) 407 } 408 409 // EmitIssue builds an issue and accumulates it 410 func (r *Runner) EmitIssue(rule Rule, message string, location hcl.Range) { 411 r.Issues = append(r.Issues, &issue.Issue{ 412 Detector: rule.Name(), 413 Type: rule.Type(), 414 Message: message, 415 Line: location.Start.Line, 416 File: r.getFileName(location.Filename), 417 Link: rule.Link(), 418 }) 419 } 420 421 // getFileName returns user-friendly file name. 422 // It returns base file name when processing root module. 423 // Otherwise, it add the module name as prefix to base file name. 424 func (r *Runner) getFileName(raw string) string { 425 if r.TFConfig.Path.IsRoot() { 426 return filepath.Base(raw) 427 } 428 return filepath.Join(r.TFConfig.Path.String(), filepath.Base(raw)) 429 } 430 431 // prepareVariableValues prepares Terraform variables from configs, input variables and environment variables. 432 // Variables in the configuration are overwritten by environment variables. 433 // Finally, they are overwritten by received input variable on the received order. 434 // Therefore, CLI flag input variables must be passed at the end of arguments. 435 // This is the responsibility of the caller. 436 // See https://www.terraform.io/intro/getting-started/variables.html#assigning-variables 437 func prepareVariableValues(configVars map[string]*configs.Variable, variables ...terraform.InputValues) map[string]map[string]cty.Value { 438 overrideVariables := terraform.DefaultVariableValues(configVars).Override(getTFEnvVariables()).Override(variables...) 439 440 variableValues := make(map[string]map[string]cty.Value) 441 variableValues[""] = make(map[string]cty.Value) 442 for k, iv := range overrideVariables { 443 variableValues[""][k] = iv.Value 444 } 445 return variableValues 446 } 447 448 func isEvaluable(expr hcl.Expression) bool { 449 refs, diags := lang.ReferencesInExpr(expr) 450 if diags.HasErrors() { 451 // Maybe this is bug 452 panic(diags.Err()) 453 } 454 for _, ref := range refs { 455 switch ref.Subject.(type) { 456 case addrs.InputVariable: 457 // noop 458 case addrs.TerraformAttr: 459 // noop 460 default: 461 return false 462 } 463 } 464 return true 465 }