github.com/terraform-linters/tflint-plugin-sdk@v0.22.0/helper/runner.go (about) 1 package helper 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 "reflect" 8 9 "github.com/hashicorp/hcl/v2" 10 "github.com/hashicorp/hcl/v2/gohcl" 11 "github.com/hashicorp/hcl/v2/hclsyntax" 12 "github.com/terraform-linters/tflint-plugin-sdk/hclext" 13 "github.com/terraform-linters/tflint-plugin-sdk/internal" 14 "github.com/terraform-linters/tflint-plugin-sdk/terraform/addrs" 15 "github.com/terraform-linters/tflint-plugin-sdk/terraform/lang/marks" 16 "github.com/terraform-linters/tflint-plugin-sdk/tflint" 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 is a mock that satisfies the Runner interface for plugin testing. 23 type Runner struct { 24 Issues Issues 25 26 files map[string]*hcl.File 27 sources map[string][]byte 28 config Config 29 variables map[string]*Variable 30 fixer *internal.Fixer 31 } 32 33 // Variable is an implementation of variables in Terraform language 34 type Variable struct { 35 Name string 36 Default cty.Value 37 DeclRange hcl.Range 38 } 39 40 // Config is a pseudo TFLint config file object for testing from plugins. 41 type Config struct { 42 Rules []RuleConfig `hcl:"rule,block"` 43 } 44 45 // RuleConfig is a pseudo TFLint config file object for testing from plugins. 46 type RuleConfig struct { 47 Name string `hcl:"name,label"` 48 Enabled bool `hcl:"enabled"` 49 Body hcl.Body `hcl:",remain"` 50 } 51 52 var _ tflint.Runner = &Runner{} 53 54 // GetOriginalwd always returns the current directory 55 func (r *Runner) GetOriginalwd() (string, error) { 56 return os.Getwd() 57 } 58 59 // GetModulePath always returns the root module path address 60 func (r *Runner) GetModulePath() (addrs.Module, error) { 61 return []string{}, nil 62 } 63 64 // GetModuleContent gets a content of the current module 65 func (r *Runner) GetModuleContent(schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, error) { 66 content := &hclext.BodyContent{} 67 diags := hcl.Diagnostics{} 68 69 for _, f := range r.files { 70 c, d := hclext.PartialContent(f.Body, schema) 71 diags = diags.Extend(d) 72 for name, attr := range c.Attributes { 73 content.Attributes[name] = attr 74 } 75 content.Blocks = append(content.Blocks, c.Blocks...) 76 } 77 78 if diags.HasErrors() { 79 return nil, diags 80 } 81 return content, nil 82 } 83 84 // GetResourceContent gets a resource content of the current module 85 func (r *Runner) GetResourceContent(name string, schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, error) { 86 body, err := r.GetModuleContent(&hclext.BodySchema{ 87 Blocks: []hclext.BlockSchema{ 88 {Type: "resource", LabelNames: []string{"type", "name"}, Body: schema}, 89 }, 90 }, opts) 91 if err != nil { 92 return nil, err 93 } 94 95 content := &hclext.BodyContent{Blocks: []*hclext.Block{}} 96 for _, resource := range body.Blocks { 97 if resource.Labels[0] != name { 98 continue 99 } 100 101 content.Blocks = append(content.Blocks, resource) 102 } 103 104 return content, nil 105 } 106 107 // GetProviderContent gets a provider content of the current module 108 func (r *Runner) GetProviderContent(name string, schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, error) { 109 body, err := r.GetModuleContent(&hclext.BodySchema{ 110 Blocks: []hclext.BlockSchema{ 111 {Type: "provider", LabelNames: []string{"name"}, Body: schema}, 112 }, 113 }, opts) 114 if err != nil { 115 return nil, err 116 } 117 118 content := &hclext.BodyContent{Blocks: []*hclext.Block{}} 119 for _, provider := range body.Blocks { 120 if provider.Labels[0] != name { 121 continue 122 } 123 124 content.Blocks = append(content.Blocks, provider) 125 } 126 127 return content, nil 128 } 129 130 // GetFile returns the hcl.File object 131 func (r *Runner) GetFile(filename string) (*hcl.File, error) { 132 return r.files[filename], nil 133 } 134 135 // GetFiles returns all hcl.File 136 func (r *Runner) GetFiles() (map[string]*hcl.File, error) { 137 return r.files, nil 138 } 139 140 type nativeWalker struct { 141 walker tflint.ExprWalker 142 } 143 144 func (w *nativeWalker) Enter(node hclsyntax.Node) hcl.Diagnostics { 145 if expr, ok := node.(hcl.Expression); ok { 146 return w.walker.Enter(expr) 147 } 148 return nil 149 } 150 151 func (w *nativeWalker) Exit(node hclsyntax.Node) hcl.Diagnostics { 152 if expr, ok := node.(hcl.Expression); ok { 153 return w.walker.Exit(expr) 154 } 155 return nil 156 } 157 158 // WalkExpressions traverses expressions in all files by the passed walker. 159 func (r *Runner) WalkExpressions(walker tflint.ExprWalker) hcl.Diagnostics { 160 diags := hcl.Diagnostics{} 161 for _, file := range r.files { 162 if body, ok := file.Body.(*hclsyntax.Body); ok { 163 walkDiags := hclsyntax.Walk(body, &nativeWalker{walker: walker}) 164 diags = diags.Extend(walkDiags) 165 continue 166 } 167 168 // In JSON syntax, everything can be walked as an attribute. 169 attrs, jsonDiags := file.Body.JustAttributes() 170 if jsonDiags.HasErrors() { 171 diags = diags.Extend(jsonDiags) 172 continue 173 } 174 175 for _, attr := range attrs { 176 enterDiags := walker.Enter(attr.Expr) 177 diags = diags.Extend(enterDiags) 178 exitDiags := walker.Exit(attr.Expr) 179 diags = diags.Extend(exitDiags) 180 } 181 } 182 183 return diags 184 } 185 186 // DecodeRuleConfig extracts the rule's configuration into the given value 187 func (r *Runner) DecodeRuleConfig(name string, ret interface{}) error { 188 schema := hclext.ImpliedBodySchema(ret) 189 190 for _, rule := range r.config.Rules { 191 if rule.Name == name { 192 body, diags := hclext.Content(rule.Body, schema) 193 if diags.HasErrors() { 194 return diags 195 } 196 if diags := hclext.DecodeBody(body, nil, ret); diags.HasErrors() { 197 return diags 198 } 199 return nil 200 } 201 } 202 203 return nil 204 } 205 206 var errRefTy = reflect.TypeOf((*error)(nil)).Elem() 207 208 // EvaluateExpr returns a value of the passed expression. 209 // Note that some features are limited 210 func (r *Runner) EvaluateExpr(expr hcl.Expression, target interface{}, opts *tflint.EvaluateExprOption) error { 211 rval := reflect.ValueOf(target) 212 rty := rval.Type() 213 214 var callback bool 215 switch rty.Kind() { 216 case reflect.Func: 217 // Callback must meet the following requirements: 218 // - It must be a function 219 // - It must take an argument 220 // - It must return an error 221 if !(rty.NumIn() == 1 && rty.NumOut() == 1 && rty.Out(0).Implements(errRefTy)) { 222 panic(`callback must be of type "func (v T) error"`) 223 } 224 callback = true 225 target = reflect.New(rty.In(0)).Interface() 226 227 case reflect.Pointer: 228 // ok 229 default: 230 panic("target value is not a pointer or function") 231 } 232 233 err := r.evaluateExpr(expr, target, opts) 234 if !callback { 235 // error should be handled in the caller 236 return err 237 } 238 239 if err != nil { 240 // If it cannot be represented as a Go value, exit without invoking the callback rather than returning an error. 241 if errors.Is(err, tflint.ErrUnknownValue) || errors.Is(err, tflint.ErrNullValue) || errors.Is(err, tflint.ErrSensitive) || errors.Is(err, tflint.ErrUnevaluable) { 242 return nil 243 } 244 return err 245 } 246 247 rerr := rval.Call([]reflect.Value{reflect.ValueOf(target).Elem()}) 248 if rerr[0].IsNil() { 249 return nil 250 } 251 return rerr[0].Interface().(error) 252 } 253 254 func (r *Runner) evaluateExpr(expr hcl.Expression, target interface{}, opts *tflint.EvaluateExprOption) error { 255 if opts == nil { 256 opts = &tflint.EvaluateExprOption{} 257 } 258 259 var ty cty.Type 260 if opts.WantType != nil { 261 ty = *opts.WantType 262 } else { 263 switch target.(type) { 264 case *string: 265 ty = cty.String 266 case *int: 267 ty = cty.Number 268 case *bool: 269 ty = cty.Bool 270 case *[]string: 271 ty = cty.List(cty.String) 272 case *[]int: 273 ty = cty.List(cty.Number) 274 case *[]bool: 275 ty = cty.List(cty.Bool) 276 case *map[string]string: 277 ty = cty.Map(cty.String) 278 case *map[string]int: 279 ty = cty.Map(cty.Number) 280 case *map[string]bool: 281 ty = cty.Map(cty.Bool) 282 case *cty.Value: 283 ty = cty.DynamicPseudoType 284 default: 285 return fmt.Errorf("unsupported target type: %T", target) 286 } 287 } 288 289 variables := map[string]cty.Value{} 290 for _, variable := range r.variables { 291 variables[variable.Name] = variable.Default 292 } 293 workspace, success := os.LookupEnv("TF_WORKSPACE") 294 if !success { 295 workspace = "default" 296 } 297 rawVal, diags := expr.Value(&hcl.EvalContext{ 298 Variables: map[string]cty.Value{ 299 "var": cty.ObjectVal(variables), 300 "terraform": cty.ObjectVal(map[string]cty.Value{ 301 "workspace": cty.StringVal(workspace), 302 }), 303 }, 304 }) 305 if diags.HasErrors() { 306 return diags 307 } 308 val, err := convert.Convert(rawVal, ty) 309 if err != nil { 310 return err 311 } 312 313 return gocty.FromCtyValue(val, target) 314 } 315 316 // EmitIssue adds an issue to the runner itself. 317 func (r *Runner) EmitIssue(rule tflint.Rule, message string, location hcl.Range) error { 318 r.Issues = append(r.Issues, &Issue{ 319 Rule: rule, 320 Message: message, 321 Range: location, 322 }) 323 return nil 324 } 325 326 // EmitIssueWithFix adds an issue and invoke fix. 327 func (r *Runner) EmitIssueWithFix(rule tflint.Rule, message string, location hcl.Range, fixFunc func(f tflint.Fixer) error) error { 328 r.fixer.StashChanges() 329 if err := fixFunc(r.fixer); err != nil { 330 if errors.Is(err, tflint.ErrFixNotSupported) { 331 r.fixer.PopChangesFromStash() 332 return r.EmitIssue(rule, message, location) 333 } 334 return err 335 } 336 return r.EmitIssue(rule, message, location) 337 } 338 339 // Changes returns formatted changes by the fixer. 340 func (r *Runner) Changes() map[string][]byte { 341 r.fixer.FormatChanges() 342 return r.fixer.Changes() 343 } 344 345 // EnsureNoError is a method that simply runs a function if there is no error. 346 // 347 // Deprecated: Use EvaluateExpr with a function callback. e.g. EvaluateExpr(expr, func (val T) error {}, ...) 348 func (r *Runner) EnsureNoError(err error, proc func() error) error { 349 if err == nil { 350 return proc() 351 } 352 return err 353 } 354 355 // newLocalRunner initialises a new test runner. 356 func newLocalRunner(files map[string]*hcl.File, issues Issues) *Runner { 357 return &Runner{ 358 files: map[string]*hcl.File{}, 359 sources: map[string][]byte{}, 360 variables: map[string]*Variable{}, 361 Issues: issues, 362 } 363 } 364 365 // addLocalFile adds a new file to the current mapped files. 366 // For testing only. Normally, the main TFLint process is responsible for loading files. 367 func (r *Runner) addLocalFile(name string, file *hcl.File) bool { 368 if _, exists := r.files[name]; exists { 369 return false 370 } 371 372 r.files[name] = file 373 r.sources[name] = file.Bytes 374 return true 375 } 376 377 // initFromFiles initializes the runner from locally added files. 378 // For testing only. 379 func (r *Runner) initFromFiles() error { 380 for _, file := range r.files { 381 content, _, diags := file.Body.PartialContent(configFileSchema) 382 if diags.HasErrors() { 383 return diags 384 } 385 386 for _, block := range content.Blocks { 387 switch block.Type { 388 case "variable": 389 variable, diags := decodeVariableBlock(block) 390 if diags.HasErrors() { 391 return diags 392 } 393 r.variables[variable.Name] = variable 394 default: 395 continue 396 } 397 } 398 } 399 r.fixer = internal.NewFixer(r.sources) 400 401 return nil 402 } 403 404 func decodeVariableBlock(block *hcl.Block) (*Variable, hcl.Diagnostics) { 405 v := &Variable{ 406 Name: block.Labels[0], 407 DeclRange: block.DefRange, 408 } 409 410 content, _, diags := block.Body.PartialContent(&hcl.BodySchema{ 411 Attributes: []hcl.AttributeSchema{ 412 { 413 Name: "default", 414 }, 415 { 416 Name: "sensitive", 417 }, 418 { 419 Name: "ephemeral", 420 }, 421 }, 422 }) 423 if diags.HasErrors() { 424 return v, diags 425 } 426 427 if attr, exists := content.Attributes["default"]; exists { 428 val, diags := attr.Expr.Value(nil) 429 if diags.HasErrors() { 430 return v, diags 431 } 432 433 v.Default = val 434 } 435 if attr, exists := content.Attributes["sensitive"]; exists { 436 var sensitive bool 437 diags := gohcl.DecodeExpression(attr.Expr, nil, &sensitive) 438 if diags.HasErrors() { 439 return v, diags 440 } 441 442 v.Default = v.Default.Mark(marks.Sensitive) 443 } 444 if attr, exists := content.Attributes["ephemeral"]; exists { 445 var ephemeral bool 446 diags := gohcl.DecodeExpression(attr.Expr, nil, &ephemeral) 447 if diags.HasErrors() { 448 return v, diags 449 } 450 451 v.Default = v.Default.Mark(marks.Ephemeral) 452 } 453 454 return v, nil 455 } 456 457 var configFileSchema = &hcl.BodySchema{ 458 Blocks: []hcl.BlockHeaderSchema{ 459 { 460 Type: "variable", 461 LabelNames: []string{"name"}, 462 }, 463 }, 464 }