github.com/terraform-linters/tflint-plugin-sdk@v0.22.0/plugin/internal/plugin2host/client.go (about) 1 package plugin2host 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "os" 8 "reflect" 9 "strings" 10 11 "github.com/hashicorp/hcl/v2" 12 "github.com/hashicorp/hcl/v2/hclsyntax" 13 hcljson "github.com/hashicorp/hcl/v2/json" 14 "github.com/terraform-linters/tflint-plugin-sdk/hclext" 15 "github.com/terraform-linters/tflint-plugin-sdk/internal" 16 "github.com/terraform-linters/tflint-plugin-sdk/logger" 17 "github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/fromproto" 18 "github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/proto" 19 "github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/toproto" 20 "github.com/terraform-linters/tflint-plugin-sdk/terraform/addrs" 21 "github.com/terraform-linters/tflint-plugin-sdk/terraform/lang/marks" 22 "github.com/terraform-linters/tflint-plugin-sdk/tflint" 23 "github.com/zclconf/go-cty/cty" 24 "github.com/zclconf/go-cty/cty/gocty" 25 "github.com/zclconf/go-cty/cty/json" 26 "google.golang.org/grpc/codes" 27 "google.golang.org/grpc/status" 28 ) 29 30 // GRPCClient is a plugin-side implementation. Plugin can send requests through the client to host's gRPC server. 31 type GRPCClient struct { 32 Client proto.RunnerClient 33 Fixer *internal.Fixer 34 FixEnabled bool 35 } 36 37 var _ tflint.Runner = &GRPCClient{} 38 39 // GetOriginalwd gets the original working directory. 40 func (c *GRPCClient) GetOriginalwd() (string, error) { 41 resp, err := c.Client.GetOriginalwd(context.Background(), &proto.GetOriginalwd_Request{}) 42 if err != nil { 43 if st, ok := status.FromError(err); ok && st.Code() == codes.Unimplemented { 44 // Originalwd is available in TFLint v0.44+ 45 // Fallback to os.Getwd() because it equals the current directory in earlier versions. 46 return os.Getwd() 47 } 48 return "", fromproto.Error(err) 49 } 50 return resp.Path, err 51 } 52 53 // GetModulePath gets the current module path address. 54 func (c *GRPCClient) GetModulePath() (addrs.Module, error) { 55 resp, err := c.Client.GetModulePath(context.Background(), &proto.GetModulePath_Request{}) 56 if err != nil { 57 return nil, fromproto.Error(err) 58 } 59 return resp.Path, err 60 } 61 62 // GetResourceContent gets the contents of resources based on the schema. 63 // This is shorthand of GetModuleContent for resources 64 func (c *GRPCClient) GetResourceContent(name string, inner *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, error) { 65 if opts == nil { 66 opts = &tflint.GetModuleContentOption{} 67 } 68 opts.Hint.ResourceType = name 69 70 body, err := c.GetModuleContent(&hclext.BodySchema{ 71 Blocks: []hclext.BlockSchema{ 72 {Type: "resource", LabelNames: []string{"type", "name"}, Body: inner}, 73 }, 74 }, opts) 75 if err != nil { 76 return nil, err 77 } 78 79 content := &hclext.BodyContent{Blocks: []*hclext.Block{}} 80 for _, resource := range body.Blocks { 81 if resource.Labels[0] != name { 82 continue 83 } 84 85 content.Blocks = append(content.Blocks, resource) 86 } 87 88 return content, nil 89 } 90 91 // GetProviderContent gets the contents of providers based on the schema. 92 // This is shorthand of GetModuleContent for providers 93 func (c *GRPCClient) GetProviderContent(name string, inner *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, error) { 94 if opts == nil { 95 opts = &tflint.GetModuleContentOption{} 96 } 97 98 body, err := c.GetModuleContent(&hclext.BodySchema{ 99 Blocks: []hclext.BlockSchema{ 100 {Type: "provider", LabelNames: []string{"name"}, Body: inner}, 101 }, 102 }, opts) 103 if err != nil { 104 return nil, err 105 } 106 107 content := &hclext.BodyContent{Blocks: []*hclext.Block{}} 108 for _, provider := range body.Blocks { 109 if provider.Labels[0] != name { 110 continue 111 } 112 113 content.Blocks = append(content.Blocks, provider) 114 } 115 116 return content, nil 117 } 118 119 // GetModuleContent gets the contents of the module based on the schema. 120 func (c *GRPCClient) GetModuleContent(schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, error) { 121 if opts == nil { 122 opts = &tflint.GetModuleContentOption{} 123 } 124 125 req := &proto.GetModuleContent_Request{ 126 Schema: toproto.BodySchema(schema), 127 Option: toproto.GetModuleContentOption(opts), 128 } 129 resp, err := c.Client.GetModuleContent(context.Background(), req) 130 if err != nil { 131 return nil, fromproto.Error(err) 132 } 133 134 body, diags := fromproto.BodyContent(resp.Content) 135 if diags.HasErrors() { 136 err = diags 137 } 138 return body, err 139 } 140 141 // GetFile returns hcl.File based on the passed file name. 142 func (c *GRPCClient) GetFile(file string) (*hcl.File, error) { 143 resp, err := c.Client.GetFile(context.Background(), &proto.GetFile_Request{Name: file}) 144 if err != nil { 145 return nil, fromproto.Error(err) 146 } 147 148 var f *hcl.File 149 var diags hcl.Diagnostics 150 if strings.HasSuffix(file, ".tf") { 151 f, diags = hclsyntax.ParseConfig(resp.File, file, hcl.InitialPos) 152 } else { 153 f, diags = hcljson.Parse(resp.File, file) 154 } 155 156 if diags.HasErrors() { 157 err = diags 158 } 159 return f, err 160 } 161 162 // GetFiles returns bytes of hcl.File in the self module context. 163 func (c *GRPCClient) GetFiles() (map[string]*hcl.File, error) { 164 resp, err := c.Client.GetFiles(context.Background(), &proto.GetFiles_Request{}) 165 if err != nil { 166 return nil, fromproto.Error(err) 167 } 168 169 files := map[string]*hcl.File{} 170 var f *hcl.File 171 var diags hcl.Diagnostics 172 for name, bytes := range resp.Files { 173 var d hcl.Diagnostics 174 if strings.HasSuffix(name, ".tf") { 175 f, d = hclsyntax.ParseConfig(bytes, name, hcl.InitialPos) 176 } else { 177 f, d = hcljson.Parse(bytes, name) 178 } 179 diags = diags.Extend(d) 180 181 files[name] = f 182 } 183 184 if diags.HasErrors() { 185 return files, diags 186 } 187 return files, nil 188 } 189 190 type nativeWalker struct { 191 walker tflint.ExprWalker 192 } 193 194 func (w *nativeWalker) Enter(node hclsyntax.Node) hcl.Diagnostics { 195 if expr, ok := node.(hcl.Expression); ok { 196 return w.walker.Enter(expr) 197 } 198 return nil 199 } 200 201 func (w *nativeWalker) Exit(node hclsyntax.Node) hcl.Diagnostics { 202 if expr, ok := node.(hcl.Expression); ok { 203 return w.walker.Exit(expr) 204 } 205 return nil 206 } 207 208 // WalkExpressions traverses expressions in all files by the passed walker. 209 // Note that it behaves differently in native HCL syntax and JSON syntax. 210 // 211 // In the HCL syntax, `var.foo` and `var.bar` in `[var.foo, var.bar]` are 212 // also passed to the walker. In other words, it traverses expressions recursively. 213 // To avoid redundant checks, the walker should check the kind of expression. 214 // 215 // In the JSON syntax, only an expression of an attribute seen from the top 216 // level of the file is passed. In other words, it doesn't traverse expressions 217 // recursively. This is a limitation of JSON syntax. 218 func (c *GRPCClient) WalkExpressions(walker tflint.ExprWalker) hcl.Diagnostics { 219 files, err := c.GetFiles() 220 if err != nil { 221 return hcl.Diagnostics{ 222 { 223 Severity: hcl.DiagError, 224 Summary: "failed to call GetFiles()", 225 Detail: err.Error(), 226 }, 227 } 228 } 229 230 diags := hcl.Diagnostics{} 231 for _, file := range files { 232 if body, ok := file.Body.(*hclsyntax.Body); ok { 233 walkDiags := hclsyntax.Walk(body, &nativeWalker{walker: walker}) 234 diags = diags.Extend(walkDiags) 235 continue 236 } 237 238 // In JSON syntax, everything can be walked as an attribute. 239 attrs, jsonDiags := file.Body.JustAttributes() 240 if jsonDiags.HasErrors() { 241 diags = diags.Extend(jsonDiags) 242 continue 243 } 244 245 for _, attr := range attrs { 246 enterDiags := walker.Enter(attr.Expr) 247 diags = diags.Extend(enterDiags) 248 exitDiags := walker.Exit(attr.Expr) 249 diags = diags.Extend(exitDiags) 250 } 251 } 252 253 return diags 254 } 255 256 // DecodeRuleConfig guesses the schema of the rule config from the passed interface and sends the schema to GRPC server. 257 // Content retrieved based on the schema is decoded into the passed interface. 258 func (c *GRPCClient) DecodeRuleConfig(name string, ret interface{}) error { 259 resp, err := c.Client.GetRuleConfigContent(context.Background(), &proto.GetRuleConfigContent_Request{ 260 Name: name, 261 Schema: toproto.BodySchema(hclext.ImpliedBodySchema(ret)), 262 }) 263 if err != nil { 264 return fromproto.Error(err) 265 } 266 267 content, diags := fromproto.BodyContent(resp.Content) 268 if diags.HasErrors() { 269 return diags 270 } 271 if content.IsEmpty() { 272 return nil 273 } 274 275 diags = hclext.DecodeBody(content, nil, ret) 276 if diags.HasErrors() { 277 return diags 278 } 279 return nil 280 } 281 282 var errRefTy = reflect.TypeOf((*error)(nil)).Elem() 283 284 // EvaluateExpr evals the passed expression based on the type. 285 // Passing a callback function instead of a value as the target will invoke the callback, 286 // passing the evaluated value to the argument. 287 func (c *GRPCClient) EvaluateExpr(expr hcl.Expression, target interface{}, opts *tflint.EvaluateExprOption) error { 288 rval := reflect.ValueOf(target) 289 rty := rval.Type() 290 291 var callback bool 292 switch rty.Kind() { 293 case reflect.Func: 294 // Callback must meet the following requirements: 295 // - It must be a function 296 // - It must take an argument 297 // - It must return an error 298 if !(rty.NumIn() == 1 && rty.NumOut() == 1 && rty.Out(0).Implements(errRefTy)) { 299 panic(`callback must be of type "func (v T) error"`) 300 } 301 callback = true 302 target = reflect.New(rty.In(0)).Interface() 303 304 case reflect.Pointer: 305 // ok 306 default: 307 panic("target value is not a pointer or function") 308 } 309 310 err := c.evaluateExpr(expr, target, opts) 311 if !callback { 312 // error should be handled in the caller 313 return err 314 } 315 316 if err != nil { 317 // If it cannot be represented as a Go value, exit without invoking the callback rather than returning an error. 318 if errors.Is(err, tflint.ErrUnknownValue) || 319 errors.Is(err, tflint.ErrNullValue) || 320 errors.Is(err, tflint.ErrSensitive) || 321 errors.Is(err, tflint.ErrEphemeral) || 322 errors.Is(err, tflint.ErrUnevaluable) { 323 return nil 324 } 325 return err 326 } 327 328 rerr := rval.Call([]reflect.Value{reflect.ValueOf(target).Elem()}) 329 if rerr[0].IsNil() { 330 return nil 331 } 332 return rerr[0].Interface().(error) 333 } 334 335 func (c *GRPCClient) evaluateExpr(expr hcl.Expression, target interface{}, opts *tflint.EvaluateExprOption) error { 336 if opts == nil { 337 opts = &tflint.EvaluateExprOption{} 338 } 339 340 var ty cty.Type 341 if opts.WantType != nil { 342 ty = *opts.WantType 343 } else { 344 switch target.(type) { 345 case *string: 346 ty = cty.String 347 case *int: 348 ty = cty.Number 349 case *bool: 350 ty = cty.Bool 351 case *[]string: 352 ty = cty.List(cty.String) 353 case *[]int: 354 ty = cty.List(cty.Number) 355 case *[]bool: 356 ty = cty.List(cty.Bool) 357 case *map[string]string: 358 ty = cty.Map(cty.String) 359 case *map[string]int: 360 ty = cty.Map(cty.Number) 361 case *map[string]bool: 362 ty = cty.Map(cty.Bool) 363 case *cty.Value: 364 ty = cty.DynamicPseudoType 365 default: 366 panic(fmt.Sprintf("unsupported target type: %T", target)) 367 } 368 } 369 tyby, err := json.MarshalType(ty) 370 if err != nil { 371 return err 372 } 373 374 file, err := c.GetFile(expr.Range().Filename) 375 if err != nil { 376 return err 377 } 378 379 resp, err := c.Client.EvaluateExpr( 380 context.Background(), 381 &proto.EvaluateExpr_Request{ 382 Expression: toproto.Expression(expr, file.Bytes), 383 Option: &proto.EvaluateExpr_Option{Type: tyby, ModuleCtx: toproto.ModuleCtxType(opts.ModuleCtx)}, 384 }, 385 ) 386 if err != nil { 387 return fromproto.Error(err) 388 } 389 390 val, err := fromproto.Value(resp.Value, ty, resp.Marks) 391 if err != nil { 392 return err 393 } 394 395 if ty == cty.DynamicPseudoType { 396 return gocty.FromCtyValue(val, target) 397 } 398 399 // Returns an error if the value cannot be decoded to a Go value (e.g. unknown, null, marked). 400 // This allows the caller to handle the value by the errors package. 401 err = cty.Walk(val, func(path cty.Path, v cty.Value) (bool, error) { 402 if !v.IsKnown() { 403 logger.Debug(fmt.Sprintf("unknown value found in %s", expr.Range())) 404 return false, tflint.ErrUnknownValue 405 } 406 if v.IsNull() { 407 logger.Debug(fmt.Sprintf("null value found in %s", expr.Range())) 408 return false, tflint.ErrNullValue 409 } 410 if v.HasMark(marks.Sensitive) { 411 logger.Debug(fmt.Sprintf("sensitive value found in %s", expr.Range())) 412 return false, tflint.ErrSensitive 413 } 414 if v.HasMark(marks.Ephemeral) { 415 logger.Debug(fmt.Sprintf("ephemeral value found in %s", expr.Range())) 416 return false, tflint.ErrEphemeral 417 } 418 return true, nil 419 }) 420 if err != nil { 421 return err 422 } 423 424 return gocty.FromCtyValue(val, target) 425 } 426 427 // EmitIssue emits the issue with the passed rule, message, location 428 func (c *GRPCClient) EmitIssue(rule tflint.Rule, message string, location hcl.Range) error { 429 _, err := c.Client.EmitIssue(context.Background(), &proto.EmitIssue_Request{Rule: toproto.Rule(rule), Message: message, Range: toproto.Range(location)}) 430 if err != nil { 431 return fromproto.Error(err) 432 } 433 return nil 434 } 435 436 // EmitIssueWithFix emits the issue with the passed rule, message, location. 437 // Invoke the fix function and add the changes to the fixer. 438 // If the fix function returns ErrFixNotSupported, the emitted issue will not 439 // be marked as fixable. 440 func (c *GRPCClient) EmitIssueWithFix(rule tflint.Rule, message string, location hcl.Range, fixFunc func(f tflint.Fixer) error) error { 441 var fixable bool 442 var fixErr error 443 444 path, err := c.GetModulePath() 445 if err != nil { 446 return fromproto.Error(err) 447 } 448 // If the issue is not in the root module, skip the fix. 449 if path.IsRoot() { 450 fixable = true 451 c.Fixer.StashChanges() 452 453 fixErr = fixFunc(c.Fixer) 454 if errors.Is(fixErr, tflint.ErrFixNotSupported) { 455 fixable = false 456 } 457 } 458 459 resp, err := c.Client.EmitIssue(context.Background(), &proto.EmitIssue_Request{Rule: toproto.Rule(rule), Message: message, Range: toproto.Range(location), Fixable: fixable}) 460 if err != nil { 461 return fromproto.Error(err) 462 } 463 464 if !c.FixEnabled || !fixable || !resp.Applied { 465 c.Fixer.PopChangesFromStash() 466 return nil 467 } 468 return fixErr 469 } 470 471 // ApplyChanges applies the changes in the fixer to the server 472 func (c *GRPCClient) ApplyChanges() error { 473 _, err := c.Client.ApplyChanges(context.Background(), &proto.ApplyChanges_Request{Changes: c.Fixer.Changes()}) 474 if err != nil { 475 return fromproto.Error(err) 476 } 477 c.Fixer.ApplyChanges() 478 return nil 479 } 480 481 // EnsureNoError is a helper for error handling. Depending on the type of error generated by EvaluateExpr, 482 // determine whether to exit, skip, or continue. If it is continued, the passed function will be executed. 483 // 484 // Deprecated: Use errors.Is() instead to determine which errors can be ignored. 485 func (*GRPCClient) EnsureNoError(err error, proc func() error) error { 486 if err == nil { 487 return proc() 488 } 489 490 if errors.Is(err, tflint.ErrUnevaluable) || errors.Is(err, tflint.ErrNullValue) || errors.Is(err, tflint.ErrUnknownValue) || errors.Is(err, tflint.ErrSensitive) { 491 return nil 492 } 493 return err 494 }