-
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 (#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()
- Loading branch information
Showing
49 changed files
with
10,852 additions
and
952 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,204 @@ | ||
package hclext | ||
|
||
import ( | ||
"fmt" | ||
"reflect" | ||
"strings" | ||
|
||
"github.com/hashicorp/hcl/v2" | ||
"github.com/hashicorp/hcl/v2/gohcl" | ||
) | ||
|
||
// DecodeBody is a derivative of gohcl.DecodeBody the receives hclext.BodyContent instead of hcl.Body. | ||
// Since hcl.Body is hard to send over a wire protocol, it is needed to support BodyContent. | ||
// This method differs from gohcl.DecodeBody in several ways: | ||
// | ||
// - Does not support decoding to map, cty.Value, hcl.Body, hcl.Expression. | ||
// - Does not support `body` and `remain` tags. | ||
// - Extraneous attributes are always ignored. | ||
// | ||
// @see https://github.com/hashicorp/hcl/blob/v2.11.1/gohcl/decode.go | ||
func DecodeBody(body *BodyContent, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics { | ||
rv := reflect.ValueOf(val) | ||
if rv.Kind() != reflect.Ptr { | ||
panic(fmt.Sprintf("target value must be a pointer, not %s", rv.Type().String())) | ||
} | ||
|
||
return decodeBody(body, ctx, rv.Elem()) | ||
} | ||
|
||
func decodeBody(body *BodyContent, ctx *hcl.EvalContext, val reflect.Value) hcl.Diagnostics { | ||
if body == nil { | ||
return nil | ||
} | ||
|
||
et := val.Type() | ||
switch et.Kind() { | ||
case reflect.Struct: | ||
return decodeBodyToStruct(body, ctx, val) | ||
default: | ||
panic(fmt.Sprintf("target value must be pointer to struct, not %s", et.String())) | ||
} | ||
} | ||
|
||
func decodeBodyToStruct(body *BodyContent, ctx *hcl.EvalContext, val reflect.Value) hcl.Diagnostics { | ||
var diags hcl.Diagnostics | ||
|
||
tags := getFieldTags(val.Type()) | ||
|
||
for name, fieldIdx := range tags.Attributes { | ||
attr, exists := body.Attributes[name] | ||
if !exists { | ||
if tags.Optional[name] || val.Type().Field(fieldIdx).Type.Kind() == reflect.Ptr { | ||
// noop | ||
} else { | ||
diags = append(diags, &hcl.Diagnostic{ | ||
Severity: hcl.DiagError, | ||
Summary: fmt.Sprintf("Missing %s attribute", name), | ||
Detail: fmt.Sprintf("%s is required, but not defined here", name), | ||
}) | ||
} | ||
continue | ||
} | ||
diags = diags.Extend(gohcl.DecodeExpression(attr.Expr, ctx, val.Field(fieldIdx).Addr().Interface())) | ||
} | ||
|
||
blocksByType := body.Blocks.ByType() | ||
|
||
for typeName, fieldIdx := range tags.Blocks { | ||
blocks := blocksByType[typeName] | ||
field := val.Type().Field((fieldIdx)) | ||
|
||
ty := field.Type | ||
isSlice := false | ||
isPtr := false | ||
if ty.Kind() == reflect.Slice { | ||
isSlice = true | ||
ty = ty.Elem() | ||
} | ||
if ty.Kind() == reflect.Ptr { | ||
isPtr = true | ||
ty = ty.Elem() | ||
} | ||
|
||
if len(blocks) > 1 && !isSlice { | ||
diags = append(diags, &hcl.Diagnostic{ | ||
Severity: hcl.DiagError, | ||
Summary: fmt.Sprintf("Duplicate %s block", typeName), | ||
Detail: fmt.Sprintf( | ||
"Only one %s block is allowed. Another was defined at %s.", | ||
typeName, blocks[0].DefRange.String(), | ||
), | ||
Subject: &blocks[1].DefRange, | ||
}) | ||
continue | ||
} | ||
|
||
if len(blocks) == 0 { | ||
if isSlice || isPtr { | ||
if val.Field(fieldIdx).IsNil() { | ||
val.Field(fieldIdx).Set(reflect.Zero(field.Type)) | ||
} | ||
} else { | ||
diags = append(diags, &hcl.Diagnostic{ | ||
Severity: hcl.DiagError, | ||
Summary: fmt.Sprintf("Missing %s block", typeName), | ||
Detail: fmt.Sprintf("A %s block is required.", typeName), | ||
}) | ||
} | ||
continue | ||
} | ||
|
||
switch { | ||
|
||
case isSlice: | ||
elemType := ty | ||
if isPtr { | ||
elemType = reflect.PtrTo(ty) | ||
} | ||
sli := val.Field(fieldIdx) | ||
if sli.IsNil() { | ||
sli = reflect.MakeSlice(reflect.SliceOf(elemType), len(blocks), len(blocks)) | ||
} | ||
|
||
for i, block := range blocks { | ||
if isPtr { | ||
if i >= sli.Len() { | ||
sli = reflect.Append(sli, reflect.New(ty)) | ||
} | ||
v := sli.Index(i) | ||
if v.IsNil() { | ||
v = reflect.New(ty) | ||
} | ||
diags = append(diags, decodeBlockToValue(block, ctx, v.Elem())...) | ||
sli.Index(i).Set(v) | ||
} else { | ||
if i >= sli.Len() { | ||
sli = reflect.Append(sli, reflect.Indirect(reflect.New(ty))) | ||
} | ||
diags = append(diags, decodeBlockToValue(block, ctx, sli.Index(i))...) | ||
} | ||
} | ||
|
||
if sli.Len() > len(blocks) { | ||
sli.SetLen(len(blocks)) | ||
} | ||
|
||
val.Field(fieldIdx).Set(sli) | ||
|
||
default: | ||
block := blocks[0] | ||
if isPtr { | ||
v := val.Field(fieldIdx) | ||
if v.IsNil() { | ||
v = reflect.New(ty) | ||
} | ||
diags = append(diags, decodeBlockToValue(block, ctx, v.Elem())...) | ||
val.Field(fieldIdx).Set(v) | ||
} else { | ||
diags = append(diags, decodeBlockToValue(block, ctx, val.Field(fieldIdx))...) | ||
} | ||
|
||
} | ||
} | ||
|
||
return diags | ||
} | ||
|
||
func decodeBlockToValue(block *Block, ctx *hcl.EvalContext, v reflect.Value) hcl.Diagnostics { | ||
diags := decodeBody(block.Body, ctx, v) | ||
|
||
blockTags := getFieldTags(v.Type()) | ||
|
||
if len(block.Labels) > len(blockTags.Labels) { | ||
expectedLabels := make([]string, len(blockTags.Labels)) | ||
for i, label := range blockTags.Labels { | ||
expectedLabels[i] = label.Name | ||
} | ||
return append(diags, &hcl.Diagnostic{ | ||
Severity: hcl.DiagError, | ||
Summary: fmt.Sprintf("Extraneous label for %s", block.Type), | ||
Detail: fmt.Sprintf("Only %d labels (%s) are expected for %s blocks.", len(blockTags.Labels), strings.Join(expectedLabels, ", "), block.Type), | ||
Subject: &block.DefRange, | ||
}) | ||
} | ||
if len(block.Labels) < len(blockTags.Labels) { | ||
expectedLabels := make([]string, len(blockTags.Labels)) | ||
for i, label := range blockTags.Labels { | ||
expectedLabels[i] = label.Name | ||
} | ||
return append(diags, &hcl.Diagnostic{ | ||
Severity: hcl.DiagError, | ||
Summary: fmt.Sprintf("Missing label for %s", block.Type), | ||
Detail: fmt.Sprintf("All %s blocks must be have %d labels (%s).", block.Type, len(blockTags.Labels), strings.Join(expectedLabels, ", ")), | ||
Subject: &block.DefRange, | ||
}) | ||
} | ||
|
||
for li, lv := range block.Labels { | ||
lfieldIdx := blockTags.Labels[li].FieldIndex | ||
v.Field(lfieldIdx).Set(reflect.ValueOf(lv)) | ||
} | ||
|
||
return diags | ||
} |
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,71 @@ | ||
package hclext | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/hashicorp/hcl/v2" | ||
"github.com/hashicorp/hcl/v2/hclsyntax" | ||
) | ||
|
||
func ExampleDecodeBody() { | ||
src := ` | ||
noodle "foo" "bar" { | ||
type = "rice" | ||
bread "baz" { | ||
type = "focaccia" | ||
baked = true | ||
} | ||
bread "quz" { | ||
type = "rye" | ||
} | ||
}` | ||
file, diags := hclsyntax.ParseConfig([]byte(src), "test.tf", hcl.InitialPos) | ||
if diags.HasErrors() { | ||
panic(diags) | ||
} | ||
|
||
type Bread struct { | ||
// The `*,label` tag matches "bread" block labels. | ||
// The count of tags should be matched to count of block labels. | ||
Name string `hclext:"name,label"` | ||
// The `type` tag matches a "type" attribute inside of "bread" block. | ||
Type string `hclext:"type"` | ||
// The `baked,optional` tag matches a "baked" attribute, but it is optional. | ||
Baked bool `hclext:"baked,optional"` | ||
} | ||
type Noodle struct { | ||
Name string `hclext:"name,label"` | ||
SubName string `hclext:"subname,label"` | ||
Type string `hclext:"type"` | ||
// The `bread,block` tag matches "bread" blocks. | ||
// Multiple blocks are allowed because the field type is slice. | ||
Breads []Bread `hclext:"bread,block"` | ||
} | ||
type Config struct { | ||
// Only 1 block must be needed because the field type is not slice, not a pointer. | ||
Noodle Noodle `hclext:"noodle,block"` | ||
} | ||
|
||
target := &Config{} | ||
|
||
schema := ImpliedBodySchema(target) | ||
body, diags := Content(file.Body, schema) | ||
if diags.HasErrors() { | ||
panic(diags) | ||
} | ||
|
||
diags = DecodeBody(body, nil, target) | ||
if diags.HasErrors() { | ||
panic(diags) | ||
} | ||
|
||
fmt.Printf("- noodle: name=%s, subname=%s type=%s\n", target.Noodle.Name, target.Noodle.SubName, target.Noodle.Type) | ||
for i, bread := range target.Noodle.Breads { | ||
fmt.Printf(" - bread[%d]: name=%s, type=%s baked=%t\n", i, bread.Name, bread.Type, bread.Baked) | ||
} | ||
// Output: | ||
// - noodle: name=foo, subname=bar type=rice | ||
// - bread[0]: name=baz, type=focaccia baked=true | ||
// - bread[1]: name=quz, type=rye baked=false | ||
} |
Oops, something went wrong.