Skip to content

Commit

Permalink
plugin: gRPC-based new plugin system
Browse files Browse the repository at this point in the history
  • Loading branch information
wata727 committed Nov 28, 2021
1 parent 11205ad commit 5b072ac
Show file tree
Hide file tree
Showing 27 changed files with 5,311 additions and 63 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
proto:
cd plugin/proto; \
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative tflint.proto
13 changes: 7 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ require (
github.com/hashicorp/hcl/v2 v2.10.0
github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734
github.com/zclconf/go-cty v1.9.0
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013
google.golang.org/grpc v1.41.0
google.golang.org/protobuf v1.27.1
)

require (
github.com/agext/levenshtein v1.2.1 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/fatih/color v1.7.0 // indirect
github.com/golang/protobuf v1.3.4 // indirect
github.com/golang/protobuf v1.5.0 // indirect
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect
github.com/mattn/go-colorable v0.1.4 // indirect
github.com/mattn/go-isatty v0.0.10 // indirect
Expand All @@ -25,11 +28,9 @@ require (
github.com/oklog/run v1.0.0 // indirect
github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect
github.com/vmihailenco/tagparser v0.1.1 // indirect
golang.org/x/net v0.0.0-20200301022130-244492dfa37a // indirect
golang.org/x/sys v0.0.0-20191008105621-543471e840be // indirect
golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd // indirect
golang.org/x/text v0.3.5 // indirect
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/appengine v1.6.5 // indirect
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 // indirect
google.golang.org/grpc v1.27.1 // indirect
)
74 changes: 66 additions & 8 deletions go.sum

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions hclext/public.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package hclext

import (
"fmt"
"strings"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/json"
)

// ParseExpression is a wrapper that calls ParseExpression of hclsyntax and json based on the file extension.
// This function specializes in parsing intermediate expressions in the file,
// so it takes into account the hack on trailing newlines in heredoc.
func ParseExpression(src []byte, filename string, start hcl.Pos) (hcl.Expression, hcl.Diagnostics) {
if strings.HasSuffix(filename, ".tf") || strings.HasSuffix(filename, ".hcl") {
// HACK: Always add a newline to avoid heredoc parse errors.
// @see https://github.com/hashicorp/hcl/issues/441
src = []byte(string(src) + "\n")
return hclsyntax.ParseExpression(src, filename, start)
}

if strings.HasSuffix(filename, ".tf.json") {
return json.ParseExpressionWithStartPos(src, filename, start)
}

return nil, hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "Unexpected file extension",
Detail: fmt.Sprintf("The file name `%s` is a file with an unexpected extension. Valid extensions are `.tf` and `.tf.json`.", filename),
},
}
}
2 changes: 1 addition & 1 deletion helper/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
// Issue is a stub that has the same structure as the actually used issue object.
// This is only used for testing, as the mock Runner doesn't depend on the actual Issue structure.
type Issue struct {
Rule tflint.Rule
Rule tflint.RPCRule
Message string
Range hcl.Range
}
Expand Down
4 changes: 2 additions & 2 deletions helper/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ func (r *Runner) IsNullExpr(expr hcl.Expression) (bool, error) {
}

// EmitIssueOnExpr adds an issue to the runner itself.
func (r *Runner) EmitIssueOnExpr(rule tflint.Rule, message string, expr hcl.Expression) error {
func (r *Runner) EmitIssueOnExpr(rule tflint.RPCRule, message string, expr hcl.Expression) error {
r.Issues = append(r.Issues, &Issue{
Rule: rule,
Message: message,
Expand All @@ -260,7 +260,7 @@ func (r *Runner) EmitIssueOnExpr(rule tflint.Rule, message string, expr hcl.Expr
}

// EmitIssue adds an issue to the runner itself.
func (r *Runner) EmitIssue(rule tflint.Rule, message string, location hcl.Range) error {
func (r *Runner) EmitIssue(rule tflint.RPCRule, message string, location hcl.Range) error {
r.Issues = append(r.Issues, &Issue{
Rule: rule,
Message: message,
Expand Down
12 changes: 6 additions & 6 deletions helper/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
)

func Test_satisfyRunnerInterface(t *testing.T) {
var runner tflint.Runner
var runner tflint.RPCRunner
runner = TestRunner(t, map[string]string{})
runner.EnsureNoError(nil, func() error { return nil })
}
Expand Down Expand Up @@ -385,11 +385,11 @@ resource "aws_instance" "foo" {

type dummyRule struct{}

func (r *dummyRule) Name() string { return "dummy_rule" }
func (r *dummyRule) Enabled() bool { return true }
func (r *dummyRule) Severity() string { return tflint.ERROR }
func (r *dummyRule) Link() string { return "" }
func (r *dummyRule) Check(tflint.Runner) error { return nil }
func (r *dummyRule) Name() string { return "dummy_rule" }
func (r *dummyRule) Enabled() bool { return true }
func (r *dummyRule) Severity() string { return tflint.ERROR }
func (r *dummyRule) Link() string { return "" }
func (r *dummyRule) Check(tflint.RPCRunner) error { return nil }

func Test_EmitIssueOnExpr(t *testing.T) {
src := `
Expand Down
252 changes: 252 additions & 0 deletions plugin/fromproto/fromproto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
package fromproto

import (
"errors"

"github.com/hashicorp/hcl/v2"
"github.com/terraform-linters/tflint-plugin-sdk/hclext"
"github.com/terraform-linters/tflint-plugin-sdk/plugin/proto"
"github.com/terraform-linters/tflint-plugin-sdk/schema"
"github.com/terraform-linters/tflint-plugin-sdk/tflint"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

// BodySchema converts proto.BodySchema to schema.BodySchema
func BodySchema(body *proto.BodySchema) *schema.BodySchema {
if body == nil {
return nil
}

attributes := make([]schema.AttributeSchema, len(body.Attributes))
for idx, attr := range body.Attributes {
attributes[idx] = schema.AttributeSchema{Name: attr.Name}
}

blocks := make([]schema.BlockSchema, len(body.Blocks))
for idx, block := range body.Blocks {
blocks[idx] = schema.BlockSchema{
Type: block.Type,
LabelNames: block.LabelNames,
Body: BodySchema(block.Body),
}
}

return &schema.BodySchema{
Attributes: attributes,
Blocks: blocks,
}
}

// BodyContent converts proto.BodyContent to schema.BodyContent
func BodyContent(body *proto.BodyContent) (*schema.BodyContent, hcl.Diagnostics) {
if body == nil {
return nil, nil
}
diags := hcl.Diagnostics{}

attributes := schema.Attributes{}
for key, attr := range body.Attributes {
expr, exprDiags := hclext.ParseExpression(attr.Expr, attr.ExprRange.Filename, Pos(attr.ExprRange.Start))
diags = diags.Extend(exprDiags)

attributes[key] = &schema.Attribute{
Name: attr.Name,
Expr: expr,
Range: Range(attr.Range),
NameRange: Range(attr.NameRange),
}
}

blocks := make(schema.Blocks, len(body.Blocks))
for idx, block := range body.Blocks {
blockBody, contentDiags := BodyContent(block.Body)
diags = diags.Extend(contentDiags)

labelRanges := make([]hcl.Range, len(block.LabelRanges))
for idx, labelRange := range block.LabelRanges {
labelRanges[idx] = Range(labelRange)
}

blocks[idx] = &schema.Block{
Type: block.Type,
Labels: block.Labels,
Body: blockBody,
DefRange: Range(block.DefRange),
TypeRange: Range(block.TypeRange),
LabelRanges: labelRanges,
}
}

return &schema.BodyContent{
Attributes: attributes,
Blocks: blocks,
}, diags
}

// RuleObject is an intermediate representation that satisfies the Rule interface.
type RuleObject struct {
Data struct {
Name string
Enabled bool
Severity string
Link string
}
}

// Name returns the rule name
func (r *RuleObject) Name() string { return r.Data.Name }

// Enabled returns whether the rule is enabled
func (r *RuleObject) Enabled() bool { return r.Data.Enabled }

// Severity returns the severify of the rule
func (r *RuleObject) Severity() string { return r.Data.Severity }

// Link returns the link of the rule documentation if exists
func (r *RuleObject) Link() string { return r.Data.Link }

// Check does nothing. This is just a method to satisfy the interface
func (r *RuleObject) Check(tflint.Runner) error { return nil }

// Rule converts proto.EmitIssue_Rule to RuleObject
func Rule(rule *proto.EmitIssue_Rule) *RuleObject {
if rule == nil {
return nil
}

return &RuleObject{
Data: struct {
Name string
Enabled bool
Severity string
Link string
}{
Name: rule.Name,
Enabled: rule.Enabled,
Severity: Severity(rule.Severity),
Link: rule.Link,
},
}
}

// Severity converts proto.EmitIssue_Severity to severity
func Severity(severity proto.EmitIssue_Severity) string {
switch severity {
case proto.EmitIssue_SEVERITY_ERROR:
return tflint.ERROR
case proto.EmitIssue_SEVERITY_WARNING:
return tflint.WARNING
case proto.EmitIssue_SEVERITY_NOTICE:
return tflint.NOTICE
}

return tflint.ERROR
}

// Range converts proto.Range to hcl.Range
func Range(rng *proto.Range) hcl.Range {
if rng == nil {
return hcl.Range{}
}

return hcl.Range{
Filename: rng.Filename,
Start: Pos(rng.Start),
End: Pos(rng.End),
}
}

// Pos converts proto.Range_Pos to hcl.Pos
func Pos(pos *proto.Range_Pos) hcl.Pos {
if pos == nil {
return hcl.Pos{}
}

return hcl.Pos{
Line: int(pos.Line),
Column: int(pos.Column),
Byte: int(pos.Byte),
}
}

// Config converts proto.ApplyGlobalConfig_Config to tflint.Config
func Config(config *proto.ApplyGlobalConfig_Config) *tflint.Config {
rules := map[string]*tflint.RuleConfig{}
for name, rule := range config.Rules {
rules[name] = &tflint.RuleConfig{Name: rule.Name, Enabled: rule.Enabled}
}
return &tflint.Config{Rules: rules, DisabledByDefault: config.DisabledByDefault}
}

// Error converts gRPC error status to tflint.Error
func Error(err error) error {
st, ok := status.FromError(err)
if !ok {
return err
}

// If the error status has no details, retrieve an error from the gRPC error status.
// Remove the status code because some statuses are expected and should not be shown to users.
if len(st.Details()) == 0 {
switch st.Code() {
case codes.InvalidArgument:
fallthrough
case codes.Aborted:
return errors.New(st.Message())
default:
return err
}
}

// It is not supposed to have multiple details. The detail have an error code and are converted to tflint.Error
switch t := st.Details()[0].(type) {
case *proto.ErrorDetail:
switch t.Code {
case proto.ErrorCode_FAILED_TO_EVAL:
return &tflint.Error{
Code: tflint.EvaluationError,
Level: tflint.ErrorLevel,
Message: st.Message(),
}
case proto.ErrorCode_UNKNOWN_VALUE:
return &tflint.Error{
Code: tflint.UnknownValueError,
Level: tflint.WarningLevel,
Message: st.Message(),
}
case proto.ErrorCode_NULL_VALUE:
return &tflint.Error{
Code: tflint.NullValueError,
Level: tflint.WarningLevel,
Message: st.Message(),
}
case proto.ErrorCode_TYPE_CONVERSION:
return &tflint.Error{
Code: tflint.TypeConversionError,
Level: tflint.ErrorLevel,
Message: st.Message(),
}
case proto.ErrorCode_TYPE_MISMATCH:
return &tflint.Error{
Code: tflint.TypeMismatchError,
Level: tflint.ErrorLevel,
Message: st.Message(),
}
case proto.ErrorCode_UNEVALUABLE:
return &tflint.Error{
Code: tflint.UnevaluableError,
Level: tflint.WarningLevel,
Message: st.Message(),
}
case proto.ErrorCode_UNEXPECTED_ATTRIBUTE:
return &tflint.Error{
Code: tflint.UnexpectedAttributeError,
Level: tflint.ErrorLevel,
Message: st.Message(),
}
}
}

return err
}
Loading

0 comments on commit 5b072ac

Please sign in to comment.