github.com/wata727/tflint@v0.12.2-0.20191013070026-96dd0d36f385/tflint/runner.go (about) 1 package tflint 2 3 import ( 4 "errors" 5 "fmt" 6 "log" 7 "strings" 8 "sync" 9 10 hcl "github.com/hashicorp/hcl/v2" 11 "github.com/hashicorp/terraform/addrs" 12 "github.com/hashicorp/terraform/configs" 13 "github.com/hashicorp/terraform/configs/configschema" 14 "github.com/hashicorp/terraform/lang" 15 "github.com/hashicorp/terraform/terraform" 16 "github.com/wata727/tflint/client" 17 "github.com/zclconf/go-cty/cty" 18 "github.com/zclconf/go-cty/cty/convert" 19 "github.com/zclconf/go-cty/cty/gocty" 20 ) 21 22 // Runner checks templates according rules. 23 // For variables interplation, it has Terraform eval context. 24 // After checking, it accumulates results as issues. 25 type Runner struct { 26 TFConfig *configs.Config 27 Issues Issues 28 AwsClient *client.AwsClient 29 30 ctx terraform.BuiltinEvalContext 31 annotations map[string]Annotations 32 config *Config 33 currentExpr hcl.Expression 34 modVars map[string]*moduleVariable 35 } 36 37 // Rule is interface for building the issue 38 type Rule interface { 39 Name() string 40 Severity() string 41 Link() string 42 } 43 44 // NewRunner returns new TFLint runner 45 // It prepares built-in context (workpace metadata, variables) from 46 // received `configs.Config` and `terraform.InputValues` 47 func NewRunner(c *Config, ants map[string]Annotations, cfg *configs.Config, variables ...terraform.InputValues) (*Runner, error) { 48 path := "root" 49 if !cfg.Path.IsRoot() { 50 path = cfg.Path.String() 51 } 52 log.Printf("[INFO] Initialize new runner for %s", path) 53 54 runner := &Runner{ 55 TFConfig: cfg, 56 Issues: Issues{}, 57 AwsClient: &client.AwsClient{}, 58 59 ctx: terraform.BuiltinEvalContext{ 60 Evaluator: &terraform.Evaluator{ 61 Meta: &terraform.ContextMeta{ 62 Env: getTFWorkspace(), 63 }, 64 Config: cfg, 65 VariableValues: prepareVariableValues(cfg.Module.Variables, variables...), 66 VariableValuesLock: &sync.Mutex{}, 67 }, 68 }, 69 annotations: ants, 70 config: c, 71 } 72 73 // Initialize client for the root runner 74 if c.DeepCheck && cfg.Path.IsRoot() { 75 // FIXME: Alias providers are not considered 76 providerConfig, err := NewProviderConfig( 77 cfg.Module.ProviderConfigs["aws"], 78 runner, 79 client.AwsProviderBlockSchema, 80 ) 81 if err != nil { 82 return nil, err 83 } 84 creds, err := client.ConvertToCredentials(providerConfig) 85 if err != nil { 86 return nil, err 87 } 88 89 runner.AwsClient, err = client.NewAwsClient(c.AwsCredentials.Merge(creds)) 90 if err != nil { 91 return nil, err 92 } 93 } 94 95 return runner, nil 96 } 97 98 // NewModuleRunners returns new TFLint runners for child modules 99 // Recursively search modules and generate Runners 100 // In order to propagate attributes of moduleCall as variables to the module, 101 // evaluate the variables. If it cannot be evaluated, treat it as unknown 102 func NewModuleRunners(parent *Runner) ([]*Runner, error) { 103 runners := []*Runner{} 104 105 for name, cfg := range parent.TFConfig.Children { 106 moduleCall, ok := parent.TFConfig.Module.ModuleCalls[name] 107 if !ok { 108 panic(fmt.Errorf("Expected module call `%s` is not found in `%s`", name, parent.TFConfig.Path.String())) 109 } 110 if parent.TFConfig.Path.IsRoot() && parent.config.IgnoreModules[moduleCall.SourceAddr] { 111 log.Printf("[INFO] Ignore `%s` module", moduleCall.Name) 112 continue 113 } 114 115 attributes, diags := moduleCall.Config.JustAttributes() 116 if diags.HasErrors() { 117 var causeErr error 118 if diags[0].Subject == nil { 119 // HACK: When Subject is nil, it outputs unintended message, so it replaces with actual file. 120 causeErr = errors.New(strings.Replace(diags.Error(), "<nil>: ", "", 1)) 121 } else { 122 causeErr = diags 123 } 124 err := &Error{ 125 Code: UnexpectedAttributeError, 126 Level: ErrorLevel, 127 Message: fmt.Sprintf( 128 "Attribute of module not allowed was found in %s:%d", 129 moduleCall.DeclRange.Filename, 130 moduleCall.DeclRange.Start.Line, 131 ), 132 Cause: causeErr, 133 } 134 log.Printf("[ERROR] %s", err) 135 return runners, err 136 } 137 138 modVars := map[string]*moduleVariable{} 139 for varName, rawVar := range cfg.Module.Variables { 140 if attribute, exists := attributes[varName]; exists { 141 evalauble, err := isEvaluableExpr(attribute.Expr) 142 if err != nil { 143 return runners, err 144 } 145 146 if evalauble { 147 val, diags := parent.ctx.EvaluateExpr(attribute.Expr, cty.DynamicPseudoType, nil) 148 if diags.HasErrors() { 149 err := &Error{ 150 Code: EvaluationError, 151 Level: ErrorLevel, 152 Message: fmt.Sprintf( 153 "Failed to eval an expression in %s:%d", 154 attribute.Expr.Range().Filename, 155 attribute.Expr.Range().Start.Line, 156 ), 157 Cause: diags.Err(), 158 } 159 log.Printf("[ERROR] %s", err) 160 return runners, err 161 } 162 rawVar.Default = val 163 } else { 164 // If module attributes are not evaluable, it marks that value as unknown. 165 // Unknown values are ignored when evaluated inside the module. 166 log.Printf("[DEBUG] `%s` has been marked as unknown", varName) 167 rawVar.Default = cty.UnknownVal(cty.DynamicPseudoType) 168 } 169 170 if parent.TFConfig.Path.IsRoot() { 171 modVars[varName] = &moduleVariable{ 172 Root: true, 173 DeclRange: attribute.Expr.Range(), 174 } 175 } else { 176 parentVars := []*moduleVariable{} 177 for _, ref := range listVarRefs(attribute.Expr) { 178 if parentVar, exists := parent.modVars[ref.Name]; exists { 179 parentVars = append(parentVars, parentVar) 180 } 181 } 182 modVars[varName] = &moduleVariable{ 183 Parents: parentVars, 184 DeclRange: attribute.Expr.Range(), 185 } 186 } 187 } 188 } 189 190 runner, err := NewRunner(parent.config, parent.annotations, cfg) 191 if err != nil { 192 return runners, err 193 } 194 runner.modVars = modVars 195 // Inherit parent's AwsClient 196 runner.AwsClient = parent.AwsClient 197 runners = append(runners, runner) 198 moudleRunners, err := NewModuleRunners(runner) 199 if err != nil { 200 return runners, err 201 } 202 runners = append(runners, moudleRunners...) 203 } 204 205 return runners, nil 206 } 207 208 // EvaluateExpr is a wrapper of terraform.BultinEvalContext.EvaluateExpr and gocty.FromCtyValue 209 // When it received slice as `ret`, it converts cty.Value to expected list type 210 // because raw cty.Value has TupleType. 211 func (r *Runner) EvaluateExpr(expr hcl.Expression, ret interface{}) error { 212 evaluable, err := isEvaluableExpr(expr) 213 if err != nil { 214 err := &Error{ 215 Code: EvaluationError, 216 Level: ErrorLevel, 217 Message: fmt.Sprintf( 218 "Failed to parse an expression in %s:%d", 219 expr.Range().Filename, 220 expr.Range().Start.Line, 221 ), 222 Cause: err, 223 } 224 log.Printf("[ERROR] %s", err) 225 return err 226 } 227 228 if !evaluable { 229 err := &Error{ 230 Code: UnevaluableError, 231 Level: WarningLevel, 232 Message: fmt.Sprintf( 233 "Unevaluable expression found in %s:%d", 234 expr.Range().Filename, 235 expr.Range().Start.Line, 236 ), 237 } 238 log.Printf("[WARN] %s; TFLint ignores an unevaluable expression.", err) 239 return err 240 } 241 242 val, diags := r.ctx.EvaluateExpr(expr, cty.DynamicPseudoType, nil) 243 if diags.HasErrors() { 244 err := &Error{ 245 Code: EvaluationError, 246 Level: ErrorLevel, 247 Message: fmt.Sprintf( 248 "Failed to eval an expression in %s:%d", 249 expr.Range().Filename, 250 expr.Range().Start.Line, 251 ), 252 Cause: diags.Err(), 253 } 254 log.Printf("[ERROR] %s", err) 255 return err 256 } 257 258 err = cty.Walk(val, func(path cty.Path, v cty.Value) (bool, error) { 259 if !v.IsKnown() { 260 err := &Error{ 261 Code: UnknownValueError, 262 Level: WarningLevel, 263 Message: fmt.Sprintf( 264 "Unknown value found in %s:%d; Please use environment variables or tfvars to set the value", 265 expr.Range().Filename, 266 expr.Range().Start.Line, 267 ), 268 } 269 log.Printf("[WARN] %s; TFLint ignores an expression includes an unknown value.", err) 270 return false, err 271 } 272 273 if v.IsNull() { 274 err := &Error{ 275 Code: NullValueError, 276 Level: WarningLevel, 277 Message: fmt.Sprintf( 278 "Null value found in %s:%d", 279 expr.Range().Filename, 280 expr.Range().Start.Line, 281 ), 282 } 283 log.Printf("[WARN] %s; TFLint ignores an expression includes an null value.", err) 284 return false, err 285 } 286 287 return true, nil 288 }) 289 290 if err != nil { 291 return err 292 } 293 294 switch ret.(type) { 295 case *string: 296 val, err = convert.Convert(val, cty.String) 297 case *int: 298 val, err = convert.Convert(val, cty.Number) 299 case *[]string: 300 val, err = convert.Convert(val, cty.List(cty.String)) 301 case *[]int: 302 val, err = convert.Convert(val, cty.List(cty.Number)) 303 case *map[string]string: 304 val, err = convert.Convert(val, cty.Map(cty.String)) 305 case *map[string]int: 306 val, err = convert.Convert(val, cty.Map(cty.Number)) 307 } 308 309 if err != nil { 310 err := &Error{ 311 Code: TypeConversionError, 312 Level: ErrorLevel, 313 Message: fmt.Sprintf( 314 "Invalid type expression in %s:%d", 315 expr.Range().Filename, 316 expr.Range().Start.Line, 317 ), 318 Cause: err, 319 } 320 log.Printf("[ERROR] %s", err) 321 return err 322 } 323 324 err = gocty.FromCtyValue(val, ret) 325 if err != nil { 326 err := &Error{ 327 Code: TypeMismatchError, 328 Level: ErrorLevel, 329 Message: fmt.Sprintf( 330 "Invalid type expression in %s:%d", 331 expr.Range().Filename, 332 expr.Range().Start.Line, 333 ), 334 Cause: err, 335 } 336 log.Printf("[ERROR] %s", err) 337 return err 338 } 339 return nil 340 } 341 342 // EvaluateBlock is a wrapper of terraform.BultinEvalContext.EvaluateBlock and gocty.FromCtyValue 343 func (r *Runner) EvaluateBlock(block *hcl.Block, schema *configschema.Block, ret interface{}) error { 344 evaluable, err := isEvaluableBlock(block.Body, schema) 345 if err != nil { 346 err := &Error{ 347 Code: EvaluationError, 348 Level: ErrorLevel, 349 Message: fmt.Sprintf( 350 "Failed to parse a block in %s:%d", 351 block.DefRange.Filename, 352 block.DefRange.Start.Line, 353 ), 354 Cause: err, 355 } 356 log.Printf("[ERROR] %s", err) 357 return err 358 } 359 360 if !evaluable { 361 err := &Error{ 362 Code: UnevaluableError, 363 Level: WarningLevel, 364 Message: fmt.Sprintf( 365 "Unevaluable block found in %s:%d", 366 block.DefRange.Filename, 367 block.DefRange.Start.Line, 368 ), 369 } 370 log.Printf("[WARN] %s; TFLint ignores an unevaluable block.", err) 371 return err 372 } 373 374 val, _, diags := r.ctx.EvaluateBlock(block.Body, schema, nil, terraform.EvalDataForNoInstanceKey) 375 if diags.HasErrors() { 376 err := &Error{ 377 Code: EvaluationError, 378 Level: ErrorLevel, 379 Message: fmt.Sprintf( 380 "Failed to eval a block in %s:%d", 381 block.DefRange.Filename, 382 block.DefRange.Start.Line, 383 ), 384 Cause: diags.Err(), 385 } 386 log.Printf("[ERROR] %s", err) 387 return err 388 } 389 390 err = cty.Walk(val, func(path cty.Path, v cty.Value) (bool, error) { 391 if !v.IsKnown() { 392 err := &Error{ 393 Code: UnknownValueError, 394 Level: WarningLevel, 395 Message: fmt.Sprintf( 396 "Unknown value found in %s:%d; Please use environment variables or tfvars to set the value", 397 block.DefRange.Filename, 398 block.DefRange.Start.Line, 399 ), 400 } 401 log.Printf("[WARN] %s; TFLint ignores a block includes an unknown value.", err) 402 return false, err 403 } 404 405 return true, nil 406 }) 407 if err != nil { 408 return err 409 } 410 411 val, err = cty.Transform(val, func(path cty.Path, v cty.Value) (cty.Value, error) { 412 if v.IsNull() { 413 log.Printf( 414 "[DEBUG] Null value found in %s:%d, but TFLint treats this value as an empty value", 415 block.DefRange.Filename, 416 block.DefRange.Start.Line, 417 ) 418 return cty.StringVal(""), nil 419 } 420 return v, nil 421 }) 422 if err != nil { 423 return err 424 } 425 426 switch ret.(type) { 427 case *map[string]string: 428 val, err = convert.Convert(val, cty.Map(cty.String)) 429 case *map[string]int: 430 val, err = convert.Convert(val, cty.Map(cty.Number)) 431 } 432 433 if err != nil { 434 err := &Error{ 435 Code: TypeConversionError, 436 Level: ErrorLevel, 437 Message: fmt.Sprintf( 438 "Invalid type block in %s:%d", 439 block.DefRange.Filename, 440 block.DefRange.Start.Line, 441 ), 442 Cause: err, 443 } 444 log.Printf("[ERROR] %s", err) 445 return err 446 } 447 448 err = gocty.FromCtyValue(val, ret) 449 if err != nil { 450 err := &Error{ 451 Code: TypeMismatchError, 452 Level: ErrorLevel, 453 Message: fmt.Sprintf( 454 "Invalid type block in %s:%d", 455 block.DefRange.Filename, 456 block.DefRange.Start.Line, 457 ), 458 Cause: err, 459 } 460 log.Printf("[ERROR] %s", err) 461 return err 462 } 463 return nil 464 } 465 466 // TFConfigPath is a wrapper of addrs.Module 467 func (r *Runner) TFConfigPath() string { 468 if r.TFConfig.Path.IsRoot() { 469 return "root" 470 } 471 return r.TFConfig.Path.String() 472 } 473 474 // LookupIssues returns issues according to the received files 475 func (r *Runner) LookupIssues(files ...string) Issues { 476 if len(files) == 0 { 477 return r.Issues 478 } 479 480 issues := Issues{} 481 for _, issue := range r.Issues { 482 for _, file := range files { 483 if file == issue.Range.Filename { 484 issues = append(issues, issue) 485 } 486 } 487 } 488 return issues 489 } 490 491 // WalkResourceAttributes searches for resources and passes the appropriate attributes to the walker function 492 func (r *Runner) WalkResourceAttributes(resource, attributeName string, walker func(*hcl.Attribute) error) error { 493 for _, resource := range r.LookupResourcesByType(resource) { 494 body, _, diags := resource.Config.PartialContent(&hcl.BodySchema{ 495 Attributes: []hcl.AttributeSchema{ 496 { 497 Name: attributeName, 498 }, 499 }, 500 }) 501 if diags.HasErrors() { 502 return diags 503 } 504 505 if attribute, ok := body.Attributes[attributeName]; ok { 506 log.Printf("[DEBUG] Walk `%s` attribute", resource.Type+"."+resource.Name+"."+attributeName) 507 r.currentExpr = attribute.Expr 508 err := walker(attribute) 509 r.currentExpr = nil 510 if err != nil { 511 return err 512 } 513 } 514 } 515 516 return nil 517 } 518 519 // WalkResourceBlocks walks all blocks of the passed resource and invokes the passed function 520 func (r *Runner) WalkResourceBlocks(resource, blockType string, walker func(*hcl.Block) error) error { 521 for _, resource := range r.LookupResourcesByType(resource) { 522 body, _, diags := resource.Config.PartialContent(&hcl.BodySchema{ 523 Blocks: []hcl.BlockHeaderSchema{ 524 { 525 Type: blockType, 526 }, 527 }, 528 }) 529 if diags.HasErrors() { 530 return diags 531 } 532 533 for _, block := range body.Blocks { 534 log.Printf("[DEBUG] Walk `%s` block", resource.Type+"."+resource.Name+"."+blockType) 535 err := walker(block) 536 if err != nil { 537 return err 538 } 539 } 540 541 // Walk in the same way for dynamic blocks. Note that we are not expanding blocks. 542 // Therefore, expressions that use iterator are unevaluable. 543 dynBody, _, diags := resource.Config.PartialContent(&hcl.BodySchema{ 544 Blocks: []hcl.BlockHeaderSchema{ 545 { 546 Type: "dynamic", 547 LabelNames: []string{"name"}, 548 }, 549 }, 550 }) 551 if diags.HasErrors() { 552 return diags 553 } 554 555 for _, block := range dynBody.Blocks { 556 if len(block.Labels) == 1 && block.Labels[0] == blockType { 557 body, _, diags = block.Body.PartialContent(&hcl.BodySchema{ 558 Blocks: []hcl.BlockHeaderSchema{ 559 { 560 Type: "content", 561 }, 562 }, 563 }) 564 if diags.HasErrors() { 565 return diags 566 } 567 568 for _, block := range body.Blocks { 569 log.Printf("[DEBUG] Walk dynamic `%s` block", resource.Type+"."+resource.Name+"."+blockType) 570 err := walker(block) 571 if err != nil { 572 return err 573 } 574 } 575 } 576 } 577 } 578 579 return nil 580 } 581 582 // EnsureNoError is a helper for processing when no error occurs 583 // This function skips processing without returning an error to the caller when the error is warning 584 func (r *Runner) EnsureNoError(err error, proc func() error) error { 585 if err == nil { 586 return proc() 587 } 588 589 if appErr, ok := err.(*Error); ok { 590 switch appErr.Level { 591 case WarningLevel: 592 return nil 593 case ErrorLevel: 594 return appErr 595 default: 596 panic(appErr) 597 } 598 } else { 599 return err 600 } 601 } 602 603 // IsNullExpr check the passed expression is null 604 func (r *Runner) IsNullExpr(expr hcl.Expression) (bool, error) { 605 evaluable, err := isEvaluableExpr(expr) 606 if err != nil { 607 return false, err 608 } 609 610 if !evaluable { 611 return false, nil 612 } 613 val, diags := r.ctx.EvaluateExpr(expr, cty.DynamicPseudoType, nil) 614 if diags.HasErrors() { 615 return false, diags.Err() 616 } 617 return val.IsNull(), nil 618 } 619 620 // LookupResourcesByType returns `configs.Resource` list according to the resource type 621 func (r *Runner) LookupResourcesByType(resourceType string) []*configs.Resource { 622 ret := []*configs.Resource{} 623 624 for _, resource := range r.TFConfig.Module.ManagedResources { 625 if resource.Type == resourceType { 626 ret = append(ret, resource) 627 } 628 } 629 630 return ret 631 } 632 633 // EachStringSliceExprs iterates an evaluated value and the corresponding expression 634 // If the given expression is a static list, get an expression for each value 635 // If not, the given expression is used as it is 636 func (r *Runner) EachStringSliceExprs(expr hcl.Expression, proc func(val string, expr hcl.Expression)) error { 637 var vals []string 638 err := r.EvaluateExpr(expr, &vals) 639 640 exprs, diags := hcl.ExprList(expr) 641 if diags.HasErrors() { 642 log.Printf("[DEBUG] Expr is not static list: %s", diags) 643 for range vals { 644 exprs = append(exprs, expr) 645 } 646 } 647 648 return r.EnsureNoError(err, func() error { 649 for idx, val := range vals { 650 proc(val, exprs[idx]) 651 } 652 return nil 653 }) 654 } 655 656 // EmitIssue builds an issue and accumulates it 657 func (r *Runner) EmitIssue(rule Rule, message string, location hcl.Range) { 658 if r.TFConfig.Path.IsRoot() { 659 r.emitIssue(&Issue{ 660 Rule: rule, 661 Message: message, 662 Range: location, 663 }) 664 } else { 665 for _, modVar := range r.listModuleVars(r.currentExpr) { 666 r.emitIssue(&Issue{ 667 Rule: rule, 668 Message: message, 669 Range: modVar.DeclRange, 670 Callers: append(modVar.callers(), location), 671 }) 672 } 673 } 674 } 675 676 func (r *Runner) emitIssue(issue *Issue) { 677 if annotations, ok := r.annotations[issue.Range.Filename]; ok { 678 for _, annotation := range annotations { 679 if annotation.IsAffected(issue) { 680 log.Printf("[INFO] %s (%s) is ignored by %s", issue.Range.String(), issue.Rule.Name(), annotation.String()) 681 return 682 } 683 } 684 } 685 r.Issues = append(r.Issues, issue) 686 } 687 688 func (r *Runner) listModuleVars(expr hcl.Expression) []*moduleVariable { 689 ret := []*moduleVariable{} 690 for _, ref := range listVarRefs(expr) { 691 if modVar, exists := r.modVars[ref.Name]; exists { 692 ret = append(ret, modVar.roots()...) 693 } 694 } 695 return ret 696 } 697 698 // prepareVariableValues prepares Terraform variables from configs, input variables and environment variables. 699 // Variables in the configuration are overwritten by environment variables. 700 // Finally, they are overwritten by received input variable on the received order. 701 // Therefore, CLI flag input variables must be passed at the end of arguments. 702 // This is the responsibility of the caller. 703 // See https://www.terraform.io/intro/getting-started/variables.html#assigning-variables 704 func prepareVariableValues(configVars map[string]*configs.Variable, variables ...terraform.InputValues) map[string]map[string]cty.Value { 705 overrideVariables := terraform.DefaultVariableValues(configVars).Override(getTFEnvVariables()).Override(variables...) 706 707 variableValues := make(map[string]map[string]cty.Value) 708 variableValues[""] = make(map[string]cty.Value) 709 for k, iv := range overrideVariables { 710 variableValues[""][k] = iv.Value 711 } 712 return variableValues 713 } 714 715 func isEvaluableExpr(expr hcl.Expression) (bool, error) { 716 refs, diags := lang.ReferencesInExpr(expr) 717 if diags.HasErrors() { 718 return false, diags.Err() 719 } 720 for _, ref := range refs { 721 if !isEvaluableRef(ref) { 722 return false, nil 723 } 724 } 725 return true, nil 726 } 727 728 func isEvaluableBlock(body hcl.Body, schema *configschema.Block) (bool, error) { 729 refs, diags := lang.ReferencesInBlock(body, schema) 730 if diags.HasErrors() { 731 return false, diags.Err() 732 } 733 for _, ref := range refs { 734 if !isEvaluableRef(ref) { 735 return false, nil 736 } 737 } 738 return true, nil 739 } 740 741 func isEvaluableRef(ref *addrs.Reference) bool { 742 switch ref.Subject.(type) { 743 case addrs.InputVariable: 744 return true 745 case addrs.TerraformAttr: 746 return true 747 case addrs.PathAttr: 748 return true 749 default: 750 return false 751 } 752 } 753 754 func listVarRefs(expr hcl.Expression) []addrs.InputVariable { 755 refs, diags := lang.ReferencesInExpr(expr) 756 if diags.HasErrors() { 757 // Maybe this is bug 758 panic(diags.Err()) 759 } 760 761 ret := []addrs.InputVariable{} 762 for _, ref := range refs { 763 if varRef, ok := ref.Subject.(addrs.InputVariable); ok { 764 ret = append(ret, varRef) 765 } 766 } 767 768 return ret 769 }