github.com/jpreese/tflint@v0.19.2-0.20200908152133-b01686250fb6/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 hcl "github.com/hashicorp/hcl/v2" 12 "github.com/hashicorp/hcl/v2/gohcl" 13 "github.com/hashicorp/terraform/addrs" 14 "github.com/hashicorp/terraform/configs" 15 "github.com/hashicorp/terraform/lang" 16 "github.com/hashicorp/terraform/terraform" 17 "github.com/terraform-linters/tflint/client" 18 "github.com/zclconf/go-cty/cty" 19 ) 20 21 // Runner checks templates according rules. 22 // For variables interplation, it has Terraform eval context. 23 // After checking, it accumulates results as issues. 24 type Runner struct { 25 TFConfig *configs.Config 26 Issues Issues 27 AwsClient *client.AwsClient 28 29 ctx terraform.EvalContext 30 files map[string]*hcl.File 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, files map[string]*hcl.File, 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 ctx := terraform.BuiltinEvalContext{ 55 Evaluator: &terraform.Evaluator{ 56 Meta: &terraform.ContextMeta{ 57 Env: getTFWorkspace(), 58 }, 59 Config: cfg.Root, 60 VariableValues: prepareVariableValues(cfg, variables...), 61 VariableValuesLock: &sync.Mutex{}, 62 }, 63 } 64 65 runner := &Runner{ 66 TFConfig: cfg, 67 Issues: Issues{}, 68 AwsClient: &client.AwsClient{}, 69 70 // TODO: As described in the godoc for UnkeyedInstanceShim, 71 // it will need to be replaced now that module.for_each is supported 72 ctx: ctx.WithPath(cfg.Path.UnkeyedInstanceShim()), 73 files: files, 74 annotations: ants, 75 config: c, 76 } 77 78 // Initialize client for the root runner 79 if c.DeepCheck && cfg.Path.IsRoot() { 80 // FIXME: Alias providers are not considered 81 providerConfig, err := NewProviderConfig( 82 cfg.Module.ProviderConfigs["aws"], 83 runner, 84 client.AwsProviderBlockSchema, 85 ) 86 if err != nil { 87 return nil, err 88 } 89 creds, err := client.ConvertToCredentials(providerConfig) 90 if err != nil { 91 return nil, err 92 } 93 94 runner.AwsClient, err = client.NewAwsClient(c.AwsCredentials.Merge(creds)) 95 if err != nil { 96 return nil, err 97 } 98 } 99 100 return runner, nil 101 } 102 103 // NewModuleRunners returns new TFLint runners for child modules 104 // Recursively search modules and generate Runners 105 // In order to propagate attributes of moduleCall as variables to the module, 106 // evaluate the variables. If it cannot be evaluated, treat it as unknown 107 func NewModuleRunners(parent *Runner) ([]*Runner, error) { 108 runners := []*Runner{} 109 110 for name, cfg := range parent.TFConfig.Children { 111 moduleCall, ok := parent.TFConfig.Module.ModuleCalls[name] 112 if !ok { 113 panic(fmt.Errorf("Expected module call `%s` is not found in `%s`", name, parent.TFConfig.Path.String())) 114 } 115 if parent.TFConfig.Path.IsRoot() && parent.config.IgnoreModules[moduleCall.SourceAddr] { 116 log.Printf("[INFO] Ignore `%s` module", moduleCall.Name) 117 continue 118 } 119 120 attributes, diags := moduleCall.Config.JustAttributes() 121 if diags.HasErrors() { 122 var causeErr error 123 if diags[0].Subject == nil { 124 // HACK: When Subject is nil, it outputs unintended message, so it replaces with actual file. 125 causeErr = errors.New(strings.Replace(diags.Error(), "<nil>: ", "", 1)) 126 } else { 127 causeErr = diags 128 } 129 err := &Error{ 130 Code: UnexpectedAttributeError, 131 Level: ErrorLevel, 132 Message: fmt.Sprintf( 133 "Attribute of module not allowed was found in %s:%d", 134 moduleCall.DeclRange.Filename, 135 moduleCall.DeclRange.Start.Line, 136 ), 137 Cause: causeErr, 138 } 139 log.Printf("[ERROR] %s", err) 140 return runners, err 141 } 142 143 modVars := map[string]*moduleVariable{} 144 for varName, rawVar := range cfg.Module.Variables { 145 if attribute, exists := attributes[varName]; exists { 146 evalauble, err := isEvaluableExpr(attribute.Expr) 147 if err != nil { 148 return runners, err 149 } 150 151 if evalauble { 152 val, diags := parent.ctx.EvaluateExpr(attribute.Expr, cty.DynamicPseudoType, nil) 153 if diags.HasErrors() { 154 err := &Error{ 155 Code: EvaluationError, 156 Level: ErrorLevel, 157 Message: fmt.Sprintf( 158 "Failed to eval an expression in %s:%d", 159 attribute.Expr.Range().Filename, 160 attribute.Expr.Range().Start.Line, 161 ), 162 Cause: diags.Err(), 163 } 164 log.Printf("[ERROR] %s", err) 165 return runners, err 166 } 167 rawVar.Default = val 168 } else { 169 // If module attributes are not evaluable, it marks that value as unknown. 170 // Unknown values are ignored when evaluated inside the module. 171 log.Printf("[DEBUG] `%s` has been marked as unknown", varName) 172 rawVar.Default = cty.UnknownVal(cty.DynamicPseudoType) 173 } 174 175 if parent.TFConfig.Path.IsRoot() { 176 modVars[varName] = &moduleVariable{ 177 Root: true, 178 DeclRange: attribute.Expr.Range(), 179 } 180 } else { 181 parentVars := []*moduleVariable{} 182 for _, ref := range listVarRefs(attribute.Expr) { 183 if parentVar, exists := parent.modVars[ref.Name]; exists { 184 parentVars = append(parentVars, parentVar) 185 } 186 } 187 modVars[varName] = &moduleVariable{ 188 Parents: parentVars, 189 DeclRange: attribute.Expr.Range(), 190 } 191 } 192 } 193 } 194 195 runner, err := NewRunner(parent.config, parent.files, parent.annotations, cfg) 196 if err != nil { 197 return runners, err 198 } 199 runner.modVars = modVars 200 // Inherit parent's AwsClient 201 runner.AwsClient = parent.AwsClient 202 runners = append(runners, runner) 203 moudleRunners, err := NewModuleRunners(runner) 204 if err != nil { 205 return runners, err 206 } 207 runners = append(runners, moudleRunners...) 208 } 209 210 return runners, nil 211 } 212 213 // TFConfigPath is a wrapper of addrs.Module 214 func (r *Runner) TFConfigPath() string { 215 if r.TFConfig.Path.IsRoot() { 216 return "root" 217 } 218 return r.TFConfig.Path.String() 219 } 220 221 // LookupIssues returns issues according to the received files 222 func (r *Runner) LookupIssues(files ...string) Issues { 223 if len(files) == 0 { 224 return r.Issues 225 } 226 227 issues := Issues{} 228 for _, issue := range r.Issues { 229 for _, file := range files { 230 if file == issue.Range.Filename { 231 issues = append(issues, issue) 232 } 233 } 234 } 235 return issues 236 } 237 238 // File returns the raw *hcl.File representation of a Terraform configuration at the specified path, 239 // or nil if there path does not match any configuration. 240 func (r *Runner) File(path string) *hcl.File { 241 return r.files[path] 242 } 243 244 // Files returns the raw *hcl.File representation of all Terraform configuration in the module directory. 245 func (r *Runner) Files() map[string]*hcl.File { 246 result := make(map[string]*hcl.File) 247 for name, file := range r.files { 248 if filepath.Dir(name) == r.TFConfig.Module.SourceDir { 249 result[name] = file 250 } 251 } 252 return result 253 } 254 255 // Backend returns the backend configuration. 256 func (r *Runner) Backend() *configs.Backend { 257 return r.TFConfig.Module.Backend 258 } 259 260 // EnsureNoError is a helper for processing when no error occurs 261 // This function skips processing without returning an error to the caller when the error is warning 262 func (r *Runner) EnsureNoError(err error, proc func() error) error { 263 if err == nil { 264 return proc() 265 } 266 267 if appErr, ok := err.(*Error); ok { 268 switch appErr.Level { 269 case WarningLevel: 270 return nil 271 case ErrorLevel: 272 return appErr 273 default: 274 panic(appErr) 275 } 276 } else { 277 return err 278 } 279 } 280 281 // IsNullExpr check the passed expression is null 282 func (r *Runner) IsNullExpr(expr hcl.Expression) (bool, error) { 283 evaluable, err := isEvaluableExpr(expr) 284 if err != nil { 285 return false, err 286 } 287 288 if !evaluable { 289 return false, nil 290 } 291 val, diags := r.ctx.EvaluateExpr(expr, cty.DynamicPseudoType, nil) 292 if diags.HasErrors() { 293 return false, diags.Err() 294 } 295 return val.IsNull(), nil 296 } 297 298 // LookupResourcesByType returns `configs.Resource` list according to the resource type 299 func (r *Runner) LookupResourcesByType(resourceType string) []*configs.Resource { 300 ret := []*configs.Resource{} 301 302 for _, resource := range r.TFConfig.Module.ManagedResources { 303 if resource.Type == resourceType { 304 ret = append(ret, resource) 305 } 306 } 307 308 return ret 309 } 310 311 // EachStringSliceExprs iterates an evaluated value and the corresponding expression 312 // If the given expression is a static list, get an expression for each value 313 // If not, the given expression is used as it is 314 func (r *Runner) EachStringSliceExprs(expr hcl.Expression, proc func(val string, expr hcl.Expression)) error { 315 var vals []string 316 err := r.EvaluateExpr(expr, &vals) 317 318 exprs, diags := hcl.ExprList(expr) 319 if diags.HasErrors() { 320 log.Printf("[DEBUG] Expr is not static list: %s", diags) 321 for range vals { 322 exprs = append(exprs, expr) 323 } 324 } 325 326 return r.EnsureNoError(err, func() error { 327 for idx, val := range vals { 328 proc(val, exprs[idx]) 329 } 330 return nil 331 }) 332 } 333 334 // EmitIssue builds an issue and accumulates it 335 func (r *Runner) EmitIssue(rule Rule, message string, location hcl.Range) { 336 if r.TFConfig.Path.IsRoot() { 337 r.emitIssue(&Issue{ 338 Rule: rule, 339 Message: message, 340 Range: location, 341 }) 342 } else { 343 for _, modVar := range r.listModuleVars(r.currentExpr) { 344 r.emitIssue(&Issue{ 345 Rule: rule, 346 Message: message, 347 Range: modVar.DeclRange, 348 Callers: append(modVar.callers(), location), 349 }) 350 } 351 } 352 } 353 354 // WithExpressionContext sets the context of the passed expression currently being processed. 355 func (r *Runner) WithExpressionContext(expr hcl.Expression, proc func() error) error { 356 r.currentExpr = expr 357 err := proc() 358 r.currentExpr = nil 359 return err 360 } 361 362 // DecodeRuleConfig extracts the rule's configuration into the given value 363 func (r *Runner) DecodeRuleConfig(ruleName string, val interface{}) error { 364 if rule, exists := r.config.Rules[ruleName]; exists { 365 diags := gohcl.DecodeBody(rule.Body, nil, val) 366 if diags.HasErrors() { 367 // HACK: If you enable the rule through the CLI instead of the file, its hcl.Body will not contain valid range. 368 // @see https://github.com/hashicorp/hcl/blob/v2.5.0/merged.go#L132-L135 369 if rule.Body.MissingItemRange().Filename == "<empty>" { 370 return errors.New("This rule cannot be enabled with the `--enable-rule` option because it lacks the required configuration") 371 } 372 return diags 373 } 374 } 375 return nil 376 } 377 378 func (r *Runner) emitIssue(issue *Issue) { 379 if annotations, ok := r.annotations[issue.Range.Filename]; ok { 380 for _, annotation := range annotations { 381 if annotation.IsAffected(issue) { 382 log.Printf("[INFO] %s (%s) is ignored by %s", issue.Range.String(), issue.Rule.Name(), annotation.String()) 383 return 384 } 385 } 386 } 387 r.Issues = append(r.Issues, issue) 388 } 389 390 func (r *Runner) listModuleVars(expr hcl.Expression) []*moduleVariable { 391 ret := []*moduleVariable{} 392 for _, ref := range listVarRefs(expr) { 393 if modVar, exists := r.modVars[ref.Name]; exists { 394 ret = append(ret, modVar.roots()...) 395 } 396 } 397 return ret 398 } 399 400 // prepareVariableValues builds variableValues from configs, input variables and environment variables. 401 // Variables which declared in the configuration are overwritten by environment variables. 402 // Finally, they are overwritten by input variables in the order passed. 403 // Therefore, CLI flag input variables must be passed at the end of arguments. 404 // This is the responsibility of the caller. 405 // See https://learn.hashicorp.com/terraform/getting-started/variables.html#assigning-variables 406 func prepareVariableValues(config *configs.Config, variables ...terraform.InputValues) map[string]map[string]cty.Value { 407 moduleKey := config.Path.UnkeyedInstanceShim().String() 408 overrideVariables := terraform.DefaultVariableValues(config.Module.Variables).Override(getTFEnvVariables()).Override(variables...) 409 410 variableValues := make(map[string]map[string]cty.Value) 411 variableValues[moduleKey] = make(map[string]cty.Value) 412 for k, iv := range overrideVariables { 413 variableValues[moduleKey][k] = iv.Value 414 } 415 return variableValues 416 } 417 418 func listVarRefs(expr hcl.Expression) []addrs.InputVariable { 419 refs, diags := lang.ReferencesInExpr(expr) 420 if diags.HasErrors() { 421 // Maybe this is bug 422 panic(diags.Err()) 423 } 424 425 ret := []addrs.InputVariable{} 426 for _, ref := range refs { 427 if varRef, ok := ref.Subject.(addrs.InputVariable); ok { 428 ret = append(ret, varRef) 429 } 430 } 431 432 return ret 433 }