Skip to content

Commit d8aa6ab

Browse files
authored
plugin: gRPC-based new plugin system (#135)
* plugin: gRPC-based new plugin system * Improve error handling * Improve logging * Remove unused error code * Use enum instead of string as issue severity * Add tests and documentation for hclext package * Add tests and docs * Add host2plugin tests * Add GetFiles()
1 parent 11205ad commit d8aa6ab

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+10852
-952
lines changed

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
proto:
2+
cd plugin/proto; \
3+
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative tflint.proto

go.mod

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@ require (
1010
github.com/hashicorp/hcl/v2 v2.10.0
1111
github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734
1212
github.com/zclconf/go-cty v1.9.0
13+
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013
14+
google.golang.org/grpc v1.41.0
15+
google.golang.org/protobuf v1.27.1
1316
)
1417

1518
require (
1619
github.com/agext/levenshtein v1.2.1 // indirect
1720
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
1821
github.com/fatih/color v1.7.0 // indirect
19-
github.com/golang/protobuf v1.3.4 // indirect
22+
github.com/golang/protobuf v1.5.0 // indirect
2023
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect
2124
github.com/mattn/go-colorable v0.1.4 // indirect
2225
github.com/mattn/go-isatty v0.0.10 // indirect
@@ -25,11 +28,9 @@ require (
2528
github.com/oklog/run v1.0.0 // indirect
2629
github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect
2730
github.com/vmihailenco/tagparser v0.1.1 // indirect
28-
golang.org/x/net v0.0.0-20200301022130-244492dfa37a // indirect
29-
golang.org/x/sys v0.0.0-20191008105621-543471e840be // indirect
31+
golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect
32+
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd // indirect
3033
golang.org/x/text v0.3.5 // indirect
31-
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect
34+
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
3235
google.golang.org/appengine v1.6.5 // indirect
33-
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 // indirect
34-
google.golang.org/grpc v1.27.1 // indirect
3536
)

go.sum

Lines changed: 66 additions & 8 deletions
Large diffs are not rendered by default.

hclext/decode.go

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
package hclext
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"strings"
7+
8+
"github.com/hashicorp/hcl/v2"
9+
"github.com/hashicorp/hcl/v2/gohcl"
10+
)
11+
12+
// DecodeBody is a derivative of gohcl.DecodeBody the receives hclext.BodyContent instead of hcl.Body.
13+
// Since hcl.Body is hard to send over a wire protocol, it is needed to support BodyContent.
14+
// This method differs from gohcl.DecodeBody in several ways:
15+
//
16+
// - Does not support decoding to map, cty.Value, hcl.Body, hcl.Expression.
17+
// - Does not support `body` and `remain` tags.
18+
// - Extraneous attributes are always ignored.
19+
//
20+
// @see https://github.com/hashicorp/hcl/blob/v2.11.1/gohcl/decode.go
21+
func DecodeBody(body *BodyContent, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics {
22+
rv := reflect.ValueOf(val)
23+
if rv.Kind() != reflect.Ptr {
24+
panic(fmt.Sprintf("target value must be a pointer, not %s", rv.Type().String()))
25+
}
26+
27+
return decodeBody(body, ctx, rv.Elem())
28+
}
29+
30+
func decodeBody(body *BodyContent, ctx *hcl.EvalContext, val reflect.Value) hcl.Diagnostics {
31+
if body == nil {
32+
return nil
33+
}
34+
35+
et := val.Type()
36+
switch et.Kind() {
37+
case reflect.Struct:
38+
return decodeBodyToStruct(body, ctx, val)
39+
default:
40+
panic(fmt.Sprintf("target value must be pointer to struct, not %s", et.String()))
41+
}
42+
}
43+
44+
func decodeBodyToStruct(body *BodyContent, ctx *hcl.EvalContext, val reflect.Value) hcl.Diagnostics {
45+
var diags hcl.Diagnostics
46+
47+
tags := getFieldTags(val.Type())
48+
49+
for name, fieldIdx := range tags.Attributes {
50+
attr, exists := body.Attributes[name]
51+
if !exists {
52+
if tags.Optional[name] || val.Type().Field(fieldIdx).Type.Kind() == reflect.Ptr {
53+
// noop
54+
} else {
55+
diags = append(diags, &hcl.Diagnostic{
56+
Severity: hcl.DiagError,
57+
Summary: fmt.Sprintf("Missing %s attribute", name),
58+
Detail: fmt.Sprintf("%s is required, but not defined here", name),
59+
})
60+
}
61+
continue
62+
}
63+
diags = diags.Extend(gohcl.DecodeExpression(attr.Expr, ctx, val.Field(fieldIdx).Addr().Interface()))
64+
}
65+
66+
blocksByType := body.Blocks.ByType()
67+
68+
for typeName, fieldIdx := range tags.Blocks {
69+
blocks := blocksByType[typeName]
70+
field := val.Type().Field((fieldIdx))
71+
72+
ty := field.Type
73+
isSlice := false
74+
isPtr := false
75+
if ty.Kind() == reflect.Slice {
76+
isSlice = true
77+
ty = ty.Elem()
78+
}
79+
if ty.Kind() == reflect.Ptr {
80+
isPtr = true
81+
ty = ty.Elem()
82+
}
83+
84+
if len(blocks) > 1 && !isSlice {
85+
diags = append(diags, &hcl.Diagnostic{
86+
Severity: hcl.DiagError,
87+
Summary: fmt.Sprintf("Duplicate %s block", typeName),
88+
Detail: fmt.Sprintf(
89+
"Only one %s block is allowed. Another was defined at %s.",
90+
typeName, blocks[0].DefRange.String(),
91+
),
92+
Subject: &blocks[1].DefRange,
93+
})
94+
continue
95+
}
96+
97+
if len(blocks) == 0 {
98+
if isSlice || isPtr {
99+
if val.Field(fieldIdx).IsNil() {
100+
val.Field(fieldIdx).Set(reflect.Zero(field.Type))
101+
}
102+
} else {
103+
diags = append(diags, &hcl.Diagnostic{
104+
Severity: hcl.DiagError,
105+
Summary: fmt.Sprintf("Missing %s block", typeName),
106+
Detail: fmt.Sprintf("A %s block is required.", typeName),
107+
})
108+
}
109+
continue
110+
}
111+
112+
switch {
113+
114+
case isSlice:
115+
elemType := ty
116+
if isPtr {
117+
elemType = reflect.PtrTo(ty)
118+
}
119+
sli := val.Field(fieldIdx)
120+
if sli.IsNil() {
121+
sli = reflect.MakeSlice(reflect.SliceOf(elemType), len(blocks), len(blocks))
122+
}
123+
124+
for i, block := range blocks {
125+
if isPtr {
126+
if i >= sli.Len() {
127+
sli = reflect.Append(sli, reflect.New(ty))
128+
}
129+
v := sli.Index(i)
130+
if v.IsNil() {
131+
v = reflect.New(ty)
132+
}
133+
diags = append(diags, decodeBlockToValue(block, ctx, v.Elem())...)
134+
sli.Index(i).Set(v)
135+
} else {
136+
if i >= sli.Len() {
137+
sli = reflect.Append(sli, reflect.Indirect(reflect.New(ty)))
138+
}
139+
diags = append(diags, decodeBlockToValue(block, ctx, sli.Index(i))...)
140+
}
141+
}
142+
143+
if sli.Len() > len(blocks) {
144+
sli.SetLen(len(blocks))
145+
}
146+
147+
val.Field(fieldIdx).Set(sli)
148+
149+
default:
150+
block := blocks[0]
151+
if isPtr {
152+
v := val.Field(fieldIdx)
153+
if v.IsNil() {
154+
v = reflect.New(ty)
155+
}
156+
diags = append(diags, decodeBlockToValue(block, ctx, v.Elem())...)
157+
val.Field(fieldIdx).Set(v)
158+
} else {
159+
diags = append(diags, decodeBlockToValue(block, ctx, val.Field(fieldIdx))...)
160+
}
161+
162+
}
163+
}
164+
165+
return diags
166+
}
167+
168+
func decodeBlockToValue(block *Block, ctx *hcl.EvalContext, v reflect.Value) hcl.Diagnostics {
169+
diags := decodeBody(block.Body, ctx, v)
170+
171+
blockTags := getFieldTags(v.Type())
172+
173+
if len(block.Labels) > len(blockTags.Labels) {
174+
expectedLabels := make([]string, len(blockTags.Labels))
175+
for i, label := range blockTags.Labels {
176+
expectedLabels[i] = label.Name
177+
}
178+
return append(diags, &hcl.Diagnostic{
179+
Severity: hcl.DiagError,
180+
Summary: fmt.Sprintf("Extraneous label for %s", block.Type),
181+
Detail: fmt.Sprintf("Only %d labels (%s) are expected for %s blocks.", len(blockTags.Labels), strings.Join(expectedLabels, ", "), block.Type),
182+
Subject: &block.DefRange,
183+
})
184+
}
185+
if len(block.Labels) < len(blockTags.Labels) {
186+
expectedLabels := make([]string, len(blockTags.Labels))
187+
for i, label := range blockTags.Labels {
188+
expectedLabels[i] = label.Name
189+
}
190+
return append(diags, &hcl.Diagnostic{
191+
Severity: hcl.DiagError,
192+
Summary: fmt.Sprintf("Missing label for %s", block.Type),
193+
Detail: fmt.Sprintf("All %s blocks must be have %d labels (%s).", block.Type, len(blockTags.Labels), strings.Join(expectedLabels, ", ")),
194+
Subject: &block.DefRange,
195+
})
196+
}
197+
198+
for li, lv := range block.Labels {
199+
lfieldIdx := blockTags.Labels[li].FieldIndex
200+
v.Field(lfieldIdx).Set(reflect.ValueOf(lv))
201+
}
202+
203+
return diags
204+
}

hclext/decode_example_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package hclext
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/hashicorp/hcl/v2"
7+
"github.com/hashicorp/hcl/v2/hclsyntax"
8+
)
9+
10+
func ExampleDecodeBody() {
11+
src := `
12+
noodle "foo" "bar" {
13+
type = "rice"
14+
15+
bread "baz" {
16+
type = "focaccia"
17+
baked = true
18+
}
19+
bread "quz" {
20+
type = "rye"
21+
}
22+
}`
23+
file, diags := hclsyntax.ParseConfig([]byte(src), "test.tf", hcl.InitialPos)
24+
if diags.HasErrors() {
25+
panic(diags)
26+
}
27+
28+
type Bread struct {
29+
// The `*,label` tag matches "bread" block labels.
30+
// The count of tags should be matched to count of block labels.
31+
Name string `hclext:"name,label"`
32+
// The `type` tag matches a "type" attribute inside of "bread" block.
33+
Type string `hclext:"type"`
34+
// The `baked,optional` tag matches a "baked" attribute, but it is optional.
35+
Baked bool `hclext:"baked,optional"`
36+
}
37+
type Noodle struct {
38+
Name string `hclext:"name,label"`
39+
SubName string `hclext:"subname,label"`
40+
Type string `hclext:"type"`
41+
// The `bread,block` tag matches "bread" blocks.
42+
// Multiple blocks are allowed because the field type is slice.
43+
Breads []Bread `hclext:"bread,block"`
44+
}
45+
type Config struct {
46+
// Only 1 block must be needed because the field type is not slice, not a pointer.
47+
Noodle Noodle `hclext:"noodle,block"`
48+
}
49+
50+
target := &Config{}
51+
52+
schema := ImpliedBodySchema(target)
53+
body, diags := Content(file.Body, schema)
54+
if diags.HasErrors() {
55+
panic(diags)
56+
}
57+
58+
diags = DecodeBody(body, nil, target)
59+
if diags.HasErrors() {
60+
panic(diags)
61+
}
62+
63+
fmt.Printf("- noodle: name=%s, subname=%s type=%s\n", target.Noodle.Name, target.Noodle.SubName, target.Noodle.Type)
64+
for i, bread := range target.Noodle.Breads {
65+
fmt.Printf(" - bread[%d]: name=%s, type=%s baked=%t\n", i, bread.Name, bread.Type, bread.Baked)
66+
}
67+
// Output:
68+
// - noodle: name=foo, subname=bar type=rice
69+
// - bread[0]: name=baz, type=focaccia baked=true
70+
// - bread[1]: name=quz, type=rye baked=false
71+
}

0 commit comments

Comments
 (0)