@ -13,6 +13,7 @@ import (
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/pkg/errors"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/gocty"
)
type Opt struct {
@ -48,11 +49,17 @@ type parser struct {
attrs map [ string ] * hcl . Attribute
funcs map [ string ] * functionDef
blocks map [ string ] map [ string ] [ ] * hcl . Block
blockValues map [ * hcl . Block ] reflect . Value
blockTypes map [ string ] reflect . Type
ectx * hcl . EvalContext
progress map [ string ] struct { }
progressF map [ string ] struct { }
progressB map [ * hcl . Block ] map [ string ] struct { }
doneF map [ string ] struct { }
doneB map [ * hcl . Block ] map [ string ] struct { }
}
func ( p * parser ) loadDeps ( exp hcl . Expression , exclude map [ string ] struct { } ) hcl . Diagnostics {
@ -79,15 +86,69 @@ func (p *parser) loadDeps(exp hcl.Expression, exclude map[string]struct{}) hcl.D
if _ , ok := exclude [ v . RootName ( ) ] ; ok {
continue
}
if err := p . resolveValue ( v . RootName ( ) ) ; err != nil {
return hcl . Diagnostics {
& hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid expression" ,
Detail : err . Error ( ) ,
Subject : v . SourceRange ( ) . Ptr ( ) ,
Context : v . SourceRange ( ) . Ptr ( ) ,
} ,
if _ , ok := p . blockTypes [ v . RootName ( ) ] ; ok {
blockType := v . RootName ( )
split := v . SimpleSplit ( ) . Rel
if len ( split ) == 0 {
return hcl . Diagnostics {
& hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid expression" ,
Detail : fmt . Sprintf ( "cannot access %s as a variable" , blockType ) ,
Subject : exp . Range ( ) . Ptr ( ) ,
Context : exp . Range ( ) . Ptr ( ) ,
} ,
}
}
blockName , ok := split [ 0 ] . ( hcl . TraverseAttr )
if ! ok {
return hcl . Diagnostics {
& hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid expression" ,
Detail : fmt . Sprintf ( "cannot traverse %s without attribute" , blockType ) ,
Subject : exp . Range ( ) . Ptr ( ) ,
Context : exp . Range ( ) . Ptr ( ) ,
} ,
}
}
blocks := p . blocks [ blockType ] [ blockName . Name ]
if len ( blocks ) == 0 {
continue
}
var target * hcl . BodySchema
if len ( split ) > 1 {
if attr , ok := split [ 1 ] . ( hcl . TraverseAttr ) ; ok {
target = & hcl . BodySchema {
Attributes : [ ] hcl . AttributeSchema { { Name : attr . Name } } ,
Blocks : [ ] hcl . BlockHeaderSchema { { Type : attr . Name } } ,
}
}
}
if err := p . resolveBlock ( blocks [ 0 ] , target ) ; err != nil {
return hcl . Diagnostics {
& hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid expression" ,
Detail : err . Error ( ) ,
Subject : v . SourceRange ( ) . Ptr ( ) ,
Context : v . SourceRange ( ) . Ptr ( ) ,
} ,
}
}
} else {
if err := p . resolveValue ( v . RootName ( ) ) ; err != nil {
return hcl . Diagnostics {
& hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid expression" ,
Detail : err . Error ( ) ,
Subject : v . SourceRange ( ) . Ptr ( ) ,
Context : v . SourceRange ( ) . Ptr ( ) ,
} ,
}
}
}
}
@ -95,6 +156,8 @@ func (p *parser) loadDeps(exp hcl.Expression, exclude map[string]struct{}) hcl.D
return nil
}
// resolveFunction forces evaluation of a function, storing the result into the
// parser.
func ( p * parser ) resolveFunction ( name string ) error {
if _ , ok := p . doneF [ name ] ; ok {
return nil
@ -170,6 +233,8 @@ func (p *parser) resolveFunction(name string) error {
return nil
}
// resolveValue forces evaluation of a named value, storing the result into the
// parser.
func ( p * parser ) resolveValue ( name string ) ( err error ) {
if _ , ok := p . ectx . Variables [ name ] ; ok {
return nil
@ -248,6 +313,157 @@ func (p *parser) resolveValue(name string) (err error) {
return nil
}
// resolveBlock force evaluates a block, storing the result in the parser. If a
// target schema is provided, only the attributes and blocks present in the
// schema will be evaluated.
func ( p * parser ) resolveBlock ( block * hcl . Block , target * hcl . BodySchema ) ( err error ) {
name := block . Labels [ 0 ]
if err := p . opt . ValidateLabel ( name ) ; err != nil {
return hcl . Diagnostics {
& hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid name" ,
Detail : err . Error ( ) ,
Subject : & block . LabelRanges [ 0 ] ,
} ,
}
}
if _ , ok := p . doneB [ block ] ; ! ok {
p . doneB [ block ] = map [ string ] struct { } { }
}
if _ , ok := p . progressB [ block ] ; ! ok {
p . progressB [ block ] = map [ string ] struct { } { }
}
if target != nil {
// filter out attributes and blocks that are already evaluated
original := target
target = & hcl . BodySchema { }
for _ , a := range original . Attributes {
if _ , ok := p . doneB [ block ] [ a . Name ] ; ! ok {
target . Attributes = append ( target . Attributes , a )
}
}
for _ , b := range original . Blocks {
if _ , ok := p . doneB [ block ] [ b . Type ] ; ! ok {
target . Blocks = append ( target . Blocks , b )
}
}
if len ( target . Attributes ) == 0 && len ( target . Blocks ) == 0 {
return nil
}
}
if target != nil {
// detect reference cycles
for _ , a := range target . Attributes {
if _ , ok := p . progressB [ block ] [ a . Name ] ; ok {
return errors . Errorf ( "reference cycle not allowed for %s.%s.%s" , block . Type , name , a . Name )
}
}
for _ , b := range target . Blocks {
if _ , ok := p . progressB [ block ] [ b . Type ] ; ok {
return errors . Errorf ( "reference cycle not allowed for %s.%s.%s" , block . Type , name , b . Type )
}
}
for _ , a := range target . Attributes {
p . progressB [ block ] [ a . Name ] = struct { } { }
}
for _ , b := range target . Blocks {
p . progressB [ block ] [ b . Type ] = struct { } { }
}
}
// create a filtered body that contains only the target properties
body := func ( ) hcl . Body {
if target != nil {
return FilterIncludeBody ( block . Body , target )
}
filter := & hcl . BodySchema { }
for k := range p . doneB [ block ] {
filter . Attributes = append ( filter . Attributes , hcl . AttributeSchema { Name : k } )
filter . Blocks = append ( filter . Blocks , hcl . BlockHeaderSchema { Type : k } )
}
return FilterExcludeBody ( block . Body , filter )
}
// load dependencies from all targeted properties
t , ok := p . blockTypes [ block . Type ]
if ! ok {
return nil
}
schema , _ := gohcl . ImpliedBodySchema ( reflect . New ( t ) . Interface ( ) )
content , _ , diag := body ( ) . PartialContent ( schema )
if diag . HasErrors ( ) {
return diag
}
for _ , a := range content . Attributes {
diag := p . loadDeps ( a . Expr , nil )
if diag . HasErrors ( ) {
return diag
}
}
for _ , b := range content . Blocks {
err := p . resolveBlock ( b , nil )
if err != nil {
return err
}
}
// decode!
var output reflect . Value
if prev , ok := p . blockValues [ block ] ; ok {
output = prev
} else {
output = reflect . New ( t )
setLabel ( output , block . Labels [ 0 ] ) // early attach labels, so we can reference them
}
diag = gohcl . DecodeBody ( body ( ) , p . ectx , output . Interface ( ) )
if diag . HasErrors ( ) {
return diag
}
p . blockValues [ block ] = output
// mark all targeted properties as done
for _ , a := range content . Attributes {
p . doneB [ block ] [ a . Name ] = struct { } { }
}
for _ , b := range content . Blocks {
p . doneB [ block ] [ b . Type ] = struct { } { }
}
if target != nil {
for _ , a := range target . Attributes {
p . doneB [ block ] [ a . Name ] = struct { } { }
}
for _ , b := range target . Blocks {
p . doneB [ block ] [ b . Type ] = struct { } { }
}
}
// store the result into the evaluation context (so if can be referenced)
outputType , err := gocty . ImpliedType ( output . Interface ( ) )
if err != nil {
return err
}
outputValue , err := gocty . ToCtyValue ( output . Interface ( ) , outputType )
if err != nil {
return err
}
var m map [ string ] cty . Value
if m2 , ok := p . ectx . Variables [ block . Type ] ; ok {
m = m2 . AsValueMap ( )
}
if m == nil {
m = map [ string ] cty . Value { }
}
m [ name ] = outputValue
p . ectx . Variables [ block . Type ] = cty . MapVal ( m )
return nil
}
func Parse ( b hcl . Body , opt Opt , val interface { } ) hcl . Diagnostics {
reserved := map [ string ] struct { } { }
schema , _ := gohcl . ImpliedBodySchema ( val )
@ -284,9 +500,16 @@ func Parse(b hcl.Body, opt Opt, val interface{}) hcl.Diagnostics {
attrs : map [ string ] * hcl . Attribute { } ,
funcs : map [ string ] * functionDef { } ,
blocks : map [ string ] map [ string ] [ ] * hcl . Block { } ,
blockValues : map [ * hcl . Block ] reflect . Value { } ,
blockTypes : map [ string ] reflect . Type { } ,
progress : map [ string ] struct { } { } ,
progressF : map [ string ] struct { } { } ,
doneF : map [ string ] struct { } { } ,
progressB : map [ * hcl . Block ] map [ string ] struct { } { } ,
doneF : map [ string ] struct { } { } ,
doneB : map [ * hcl . Block ] map [ string ] struct { } { } ,
ectx : & hcl . EvalContext {
Variables : map [ string ] cty . Value { } ,
Functions : stdlibFunctions ,
@ -337,20 +560,15 @@ func Parse(b hcl.Body, opt Opt, val interface{}) hcl.Diagnostics {
_ = p . resolveValue ( k )
}
for k := range p . attrs {
if err := p . resolveValue ( k ) ; err != nil {
if diags , ok := err . ( hcl . Diagnostics ) ; ok {
return diags
}
return hcl . Diagnostics {
& hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid attribute" ,
Detail : err . Error ( ) ,
Subject : & p . attrs [ k ] . Range ,
Context : & p . attrs [ k ] . Range ,
} ,
}
for _ , a := range content . Attributes {
return hcl . Diagnostics {
& hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid attribute" ,
Detail : "global attributes currently not supported" ,
Subject : & a . Range ,
Context : & a . Range ,
} ,
}
}
@ -403,19 +621,6 @@ func Parse(b hcl.Body, opt Opt, val interface{}) hcl.Diagnostics {
}
}
for _ , a := range content . Attributes {
return hcl . Diagnostics {
& hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid attribute" ,
Detail : "global attributes currently not supported" ,
Subject : & a . Range ,
Context : & a . Range ,
} ,
}
}
m := map [ string ] map [ string ] [ ] * hcl . Block { }
for _ , b := range content . Blocks {
if len ( b . Labels ) == 0 || len ( b . Labels ) > 1 {
return hcl . Diagnostics {
@ -428,19 +633,16 @@ func Parse(b hcl.Body, opt Opt, val interface{}) hcl.Diagnostics {
} ,
}
}
bm , ok := m [ b . Type ]
bm , ok := p. blocks [ b . Type ]
if ! ok {
bm = map [ string ] [ ] * hcl . Block { }
m [ b . Type ] = bm
p. blocks [ b . Type ] = bm
}
lbl := b . Labels [ 0 ]
bm [ lbl ] = append ( bm [ lbl ] , b )
}
vt := reflect . ValueOf ( val ) . Elem ( ) . Type ( )
numFields := vt . NumField ( )
type value struct {
reflect . Value
idx int
@ -452,9 +654,11 @@ func Parse(b hcl.Body, opt Opt, val interface{}) hcl.Diagnostics {
}
types := map [ string ] field { }
for i := 0 ; i < numFields ; i ++ {
vt := reflect . ValueOf ( val ) . Elem ( ) . Type ( )
for i := 0 ; i < vt . NumField ( ) ; i ++ {
tags := strings . Split ( vt . Field ( i ) . Tag . Get ( "hcl" ) , "," )
p . blockTypes [ tags [ 0 ] ] = vt . Field ( i ) . Type . Elem ( ) . Elem ( )
types [ tags [ 0 ] ] = field {
idx : i ,
typ : vt . Field ( i ) . Type ,
@ -466,29 +670,29 @@ func Parse(b hcl.Body, opt Opt, val interface{}) hcl.Diagnostics {
for _ , b := range content . Blocks {
v := reflect . ValueOf ( val )
t , ok := types [ b . Type ]
if ! ok {
continue
}
vv := reflect . New ( t . typ . Elem ( ) . Elem ( ) )
diag := gohcl . DecodeBody ( b . Body , p . ectx , vv . Interface ( ) )
if diag . HasErrors ( ) {
diags = append ( diags , diag ... )
continue
}
if err := opt . ValidateLabel ( b . Labels [ 0 ] ) ; err != nil {
return hcl . Diagnostics {
& hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid name" ,
Detail : err . Error ( ) ,
Subject : & b . LabelRanges [ 0 ] ,
} ,
err := p . resolveBlock ( b , nil )
if err != nil {
if diag , ok := err . ( hcl . Diagnostics ) ; ok {
if diag . HasErrors ( ) {
diags = append ( diags , diag ... )
continue
}
} else {
return hcl . Diagnostics {
& hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid attribute" ,
Detail : err . Error ( ) ,
Subject : & b . LabelRanges [ 0 ] ,
Context : & b . DefRange ,
} ,
}
}
}
vv := p . blockValues [ b ]
t := types [ b . Type ]
lblIndex := setLabel ( vv , b . Labels [ 0 ] )
oldValue , exists := t . values [ b . Labels [ 0 ] ]
@ -502,7 +706,6 @@ func Parse(b hcl.Body, opt Opt, val interface{}) hcl.Diagnostics {
}
}
}
}
if exists {
if m := oldValue . Value . MethodByName ( "Merge" ) ; m . IsValid ( ) {
@ -523,6 +726,23 @@ func Parse(b hcl.Body, opt Opt, val interface{}) hcl.Diagnostics {
return diags
}
for k := range p . attrs {
if err := p . resolveValue ( k ) ; err != nil {
if diags , ok := err . ( hcl . Diagnostics ) ; ok {
return diags
}
return hcl . Diagnostics {
& hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid attribute" ,
Detail : err . Error ( ) ,
Subject : & p . attrs [ k ] . Range ,
Context : & p . attrs [ k ] . Range ,
} ,
}
}
}
return nil
}