diff --git a/go.sum b/go.sum index b2741d9..2c0a22d 100644 --- a/go.sum +++ b/go.sum @@ -73,7 +73,6 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= -github.com/zclconf/go-cty v1.2.0 h1:sPHsy7ADcIZQP3vILvTjrh74ZA175TFP5vqiNK1UmlI= github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= github.com/zclconf/go-cty v1.4.1 h1:Xzr4m4utRDhHDifag1onwwUSq32HLoLBsp+w6tD0880= github.com/zclconf/go-cty v1.4.1/go.mod h1:nHzOclRkoj++EU9ZjSrZvRG0BXIWt8c7loYc0qXAFGQ= @@ -115,7 +114,6 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= diff --git a/tflint/client.go b/tflint/client.go index fd143b7..69c6d69 100644 --- a/tflint/client.go +++ b/tflint/client.go @@ -80,6 +80,66 @@ func (c *Client) WalkResourceAttributes(resource, attributeName string, walker f return nil } +// BlocksRequest is the interface used to communicate via RPC. +type BlocksRequest struct { + Resource string + BlockName string +} + +// BlocksResponse is the interface used to communicate via RPC. +type BlocksResponse struct { + Blocks []*Block + Err error +} + +// Block is an intermediate representation of hcl.Block. +// It has an body as a string of bytes so that hcl.Body is not transferred via RPC. +type Block struct { + Type string + Labels []string + Body []byte + BodyRange hcl.Range + + DefRange hcl.Range + TypeRange hcl.Range + LabelRanges []hcl.Range +} + +// WalkResourceBlocks queries the host process, receives a list of blocks that match the conditions, +// and passes each to the walker function. +func (c *Client) WalkResourceBlocks(resource, blockName string, walker func(*hcl.Block) error) error { + log.Printf("[DEBUG] Walk `%s.*.%s` block", resource, blockName) + + var response BlocksResponse + if err := c.rpcClient.Call("Plugin.Blocks", BlocksRequest{Resource: resource, BlockName: blockName}, &response); err != nil { + return err + } + if response.Err != nil { + return response.Err + } + + for _, block := range response.Blocks { + file, diags := parseConfig(block.Body, block.BodyRange.Filename, block.BodyRange.Start) + if diags.HasErrors() { + return diags + } + b := &hcl.Block{ + Type: block.Type, + Labels: block.Labels, + Body: file.Body, + DefRange: block.DefRange, + TypeRange: block.TypeRange, + LabelRanges: block.LabelRanges, + } + + if err := walker(b); err != nil { + return err + } + } + + return nil +} + // EvalExprRequest is the interface used to communicate via RPC. type EvalExprRequest struct { Expr []byte @@ -202,3 +262,21 @@ func parseExpression(src []byte, filename string, start hcl.Pos) (hcl.Expression panic(fmt.Sprintf("Unexpected file: %s", filename)) } + +func parseConfig(src []byte, filename string, start hcl.Pos) (*hcl.File, hcl.Diagnostics) { + if strings.HasSuffix(filename, ".tf") { + return hclsyntax.ParseConfig(src, filename, start) + } + + if strings.HasSuffix(filename, ".tf.json") { + return nil, hcl.Diagnostics{ + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "JSON configuration syntax is not supported", + Subject: &hcl.Range{Filename: filename, Start: start, End: start}, + }, + } + } + + panic(fmt.Sprintf("Unexpected file: %s", filename)) +} diff --git a/tflint/client_test.go b/tflint/client_test.go index dc8a05f..7beebca 100644 --- a/tflint/client_test.go +++ b/tflint/client_test.go @@ -1,7 +1,6 @@ package tflint import ( - "encoding/gob" "errors" "io/ioutil" "net" @@ -39,6 +38,24 @@ func (*mockServer) Attributes(req *AttributesRequest, resp *AttributesResponse) return nil } +func (*mockServer) Blocks(req *BlocksRequest, resp *BlocksResponse) error { + *resp = BlocksResponse{Blocks: []*Block{ + { + Type: "resource", + Labels: []string{"aws_instance", "foo"}, + Body: []byte(`instance_type = "t2.micro"`), + BodyRange: hcl.Range{Filename: "example.tf", Start: hcl.Pos{Line: 2, Column: 3}, End: hcl.Pos{Line: 2, Column: 28}}, + DefRange: hcl.Range{Filename: "example.tf", Start: hcl.Pos{Line: 1, Column: 1}, End: hcl.Pos{Line: 1, Column: 29}}, + TypeRange: hcl.Range{Filename: "example.tf", Start: hcl.Pos{Line: 1, Column: 1}, End: hcl.Pos{Line: 1, Column: 8}}, + LabelRanges: []hcl.Range{ + {Filename: "example.tf", Start: hcl.Pos{Line: 1, Column: 10}, End: hcl.Pos{Line: 3, Column: 23}}, + {Filename: "example.tf", Start: hcl.Pos{Line: 1, Column: 25}, End: hcl.Pos{Line: 3, Column: 29}}, + }, + }, + }, Err: nil} + return nil +} + func (*mockServer) EvalExpr(req *EvalExprRequest, resp *EvalExprResponse) error { *resp = EvalExprResponse{Val: cty.StringVal("1"), Err: nil} return nil @@ -49,8 +66,6 @@ func (s *mockServer) EmitIssue(req *EmitIssueRequest, resp *interface{}) error { } func startMockServer(t *testing.T) (*Client, *mockServer) { - gob.Register(&hclsyntax.LiteralValueExpr{}) - addy, err := net.ResolveTCPAddr("tcp", "0.0.0.0:42586") if err != nil { t.Fatal(err) @@ -106,6 +121,64 @@ func Test_WalkResourceAttributes(t *testing.T) { } } +func Test_WalkResourceBlocks(t *testing.T) { + client, server := startMockServer(t) + defer server.Listener.Close() + + walked := []*hcl.Block{} + walker := func(block *hcl.Block) error { + walked = append(walked, block) + return nil + } + + if err := client.WalkResourceBlocks("foo", "bar", walker); err != nil { + t.Fatal(err) + } + + expected := []*hcl.Block{ + { + Type: "resource", + Labels: []string{"aws_instance", "foo"}, + Body: &hclsyntax.Body{ + Attributes: hclsyntax.Attributes{ + "instance_type": { + Name: "instance_type", + Expr: &hclsyntax.TemplateExpr{ + Parts: []hclsyntax.Expression{ + &hclsyntax.LiteralValueExpr{ + SrcRange: hcl.Range{Filename: "example.tf", Start: hcl.Pos{Line: 2, Column: 20}, End: hcl.Pos{Line: 2, Column: 28}}, + }, + }, + SrcRange: hcl.Range{Filename: "example.tf", Start: hcl.Pos{Line: 2, Column: 19}, End: hcl.Pos{Line: 2, Column: 29}}, + }, + SrcRange: hcl.Range{Filename: "example.tf", Start: hcl.Pos{Line: 2, Column: 3}, End: hcl.Pos{Line: 2, Column: 29}}, + NameRange: hcl.Range{Filename: "example.tf", Start: hcl.Pos{Line: 2, Column: 3}, End: hcl.Pos{Line: 2, Column: 16}}, + EqualsRange: hcl.Range{Filename: "example.tf", Start: hcl.Pos{Line: 2, Column: 17}, End: hcl.Pos{Line: 2, Column: 18}}, + }, + }, + Blocks: hclsyntax.Blocks{}, + SrcRange: hcl.Range{Filename: "example.tf", Start: hcl.Pos{Line: 2, Column: 3}, End: hcl.Pos{Line: 2, Column: 29}}, + EndRange: hcl.Range{Filename: "example.tf", Start: hcl.Pos{Line: 2, Column: 29}, End: hcl.Pos{Line: 2, Column: 29}}, + }, + DefRange: hcl.Range{Filename: "example.tf", Start: hcl.Pos{Line: 1, Column: 1}, End: hcl.Pos{Line: 1, Column: 29}}, + TypeRange: hcl.Range{Filename: "example.tf", Start: hcl.Pos{Line: 1, Column: 1}, End: hcl.Pos{Line: 1, Column: 8}}, + LabelRanges: []hcl.Range{ + {Filename: "example.tf", Start: hcl.Pos{Line: 1, Column: 10}, End: hcl.Pos{Line: 3, Column: 23}}, + {Filename: "example.tf", Start: hcl.Pos{Line: 1, Column: 25}, End: hcl.Pos{Line: 3, Column: 29}}, + }, + }, + } + + opts := []cmp.Option{ + cmpopts.IgnoreUnexported(hclsyntax.Body{}), + cmpopts.IgnoreFields(hclsyntax.LiteralValueExpr{}, "Val"), + cmpopts.IgnoreFields(hcl.Pos{}, "Byte"), + } + if !cmp.Equal(expected, walked, opts...) { + t.Fatalf("Diff: %s", cmp.Diff(expected, walked, opts...)) + } +} + func Test_EvaluateExpr(t *testing.T) { client, server := startMockServer(t) defer server.Listener.Close() diff --git a/tflint/interface.go b/tflint/interface.go index c58b421..9878820 100644 --- a/tflint/interface.go +++ b/tflint/interface.go @@ -7,6 +7,7 @@ import ( // Runner acts as a client for each plugin to query the host process about the Terraform configurations. type Runner interface { WalkResourceAttributes(string, string, func(*hcl.Attribute) error) error + WalkResourceBlocks(string, string, func(*hcl.Block) error) error EvaluateExpr(expr hcl.Expression, ret interface{}) error EmitIssue(rule Rule, message string, location hcl.Range, meta Metadata) error EnsureNoError(error, func() error) error