Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

plugin: gRPC-based new plugin system #135

Merged
merged 9 commits into from
Mar 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

204 changes: 204 additions & 0 deletions hclext/decode.go
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
}
71 changes: 71 additions & 0 deletions hclext/decode_example_test.go
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
}
Loading