-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
plugin: gRPC-based new plugin system
- Loading branch information
Showing
26 changed files
with
4,119 additions
and
54 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
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") { | ||
// 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) | ||
} | ||
|
||
panic(fmt.Sprintf("Unexpected file: %s", filename)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
package fromproto | ||
|
||
import ( | ||
"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" | ||
) | ||
|
||
// 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 // TODO: enum? | ||
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), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
package host2plugin | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"os" | ||
"os/exec" | ||
|
||
"github.com/hashicorp/go-hclog" | ||
"github.com/hashicorp/go-plugin" | ||
"github.com/terraform-linters/tflint-plugin-sdk/plugin/plugin2host" | ||
"github.com/terraform-linters/tflint-plugin-sdk/plugin/proto" | ||
"google.golang.org/grpc" | ||
"google.golang.org/grpc/codes" | ||
"google.golang.org/grpc/status" | ||
) | ||
|
||
// GRPCClient is a host-side implementation. Host can send requests through the client to plugin's gRPC server. | ||
type GRPCClient struct { | ||
broker *plugin.GRPCBroker | ||
client proto.RuleSetClient | ||
} | ||
|
||
// ClientOpts is an option for initializing a Client. | ||
type ClientOpts struct { | ||
Cmd *exec.Cmd | ||
} | ||
|
||
// NewClient is a wrapper of plugin.NewClient. | ||
func NewClient(opts *ClientOpts) *plugin.Client { | ||
return plugin.NewClient(&plugin.ClientConfig{ | ||
HandshakeConfig: handshakeConfig, | ||
Plugins: map[string]plugin.Plugin{ | ||
"ruleset": &RuleSetPlugin{}, | ||
}, | ||
Cmd: opts.Cmd, | ||
AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, | ||
// TODO: Should use structured logging? | ||
// https://github.com/hashicorp/terraform-plugin-sdk/blob/v2.8.0/plugin/serve.go#L64-L75 | ||
Logger: hclog.New(&hclog.LoggerOptions{ | ||
Name: "plugin", | ||
Output: os.Stderr, | ||
Level: hclog.LevelFromString(os.Getenv("TFLINT_LOG")), | ||
}), | ||
}) | ||
} | ||
|
||
// TODO: Improve error handling | ||
|
||
// RuleSetName returns the name of the plugin. | ||
func (c *GRPCClient) RuleSetName() (string, error) { | ||
resp, err := c.client.RuleSetName(context.Background(), &proto.RuleSetName_Request{}) | ||
if err != nil { | ||
return "", err | ||
} | ||
return resp.Name, nil | ||
} | ||
|
||
// RuleSetVersion returns the version of the plugin. | ||
func (c *GRPCClient) RuleSetVersion() (string, error) { | ||
resp, err := c.client.RuleSetVersion(context.Background(), &proto.RuleSetVersion_Request{}) | ||
if err != nil { | ||
return "", err | ||
} | ||
return resp.Version, nil | ||
} | ||
|
||
// RuleNames returns the list of rule names provided by the plugin. | ||
func (c *GRPCClient) RuleNames() ([]string, error) { | ||
resp, err := c.client.RuleNames(context.Background(), &proto.RuleNames_Request{}) | ||
if err != nil { | ||
return []string{}, err | ||
} | ||
return resp.Names, nil | ||
} | ||
|
||
// Check calls its own plugin implementation with an gRPC client that can send | ||
// requests to the host process. | ||
func (c *GRPCClient) Check(runner plugin2host.Server) error { | ||
brokerID := c.broker.NextId() | ||
go c.broker.AcceptAndServe(brokerID, func(opts []grpc.ServerOption) *grpc.Server { | ||
server := grpc.NewServer(opts...) | ||
proto.RegisterRunnerServer(server, &plugin2host.GRPCServer{Impl: runner}) | ||
return server | ||
}) | ||
|
||
_, err := c.client.Check(context.Background(), &proto.Check_Request{Runner: brokerID}) | ||
|
||
if err != nil { | ||
return fromGRPCError(err) | ||
} | ||
return nil | ||
} | ||
|
||
func fromGRPCError(err error) error { | ||
st, ok := status.FromError(err) | ||
if !ok { | ||
return err | ||
} | ||
switch st.Code() { | ||
case codes.Aborted: | ||
return errors.New(st.Message()) | ||
default: | ||
return err | ||
} | ||
} |
Oops, something went wrong.