73af6fd220
The HCL2 parser needs to apply special parsing tweaks so it can parse the task config the same way as HCL1. Particularly, it needs to reinterprets `map[string]interface{}` fields and blocks that appear when attributes are expected. This commit restricts the special casing to the Job fields, and ignore `variables` and `locals` block.
207 lines
4.4 KiB
Go
207 lines
4.4 KiB
Go
package jobspec2
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"math/big"
|
|
"reflect"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/mitchellh/reflectwalk"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
// decodeMapInterfaceType decodes hcl instances of `map[string]interface{}` fields
|
|
// of v.
|
|
//
|
|
// The HCL parser stores the hcl AST as the map values, and decodeMapInterfaceType
|
|
// evaluates the AST and converts them to the native golang types.
|
|
func decodeMapInterfaceType(v interface{}, ctx *hcl.EvalContext) hcl.Diagnostics {
|
|
w := &walker{ctx: ctx}
|
|
err := reflectwalk.Walk(v, w)
|
|
if err != nil {
|
|
w.diags = append(w.diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "unexpected internal error",
|
|
Detail: err.Error(),
|
|
})
|
|
}
|
|
return w.diags
|
|
}
|
|
|
|
type walker struct {
|
|
ctx *hcl.EvalContext
|
|
diags hcl.Diagnostics
|
|
}
|
|
|
|
var mapStringInterfaceType = reflect.TypeOf(map[string]interface{}{})
|
|
|
|
func (w *walker) Map(m reflect.Value) error {
|
|
if !m.Type().AssignableTo(mapStringInterfaceType) {
|
|
return nil
|
|
}
|
|
|
|
// ignore private map fields
|
|
if !m.CanSet() {
|
|
return nil
|
|
}
|
|
|
|
for _, k := range m.MapKeys() {
|
|
v := m.MapIndex(k)
|
|
if attr, ok := v.Interface().(*hcl.Attribute); ok {
|
|
c, diags := decodeInterface(attr.Expr, w.ctx)
|
|
w.diags = append(w.diags, diags...)
|
|
|
|
m.SetMapIndex(k, reflect.ValueOf(c))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (w *walker) MapElem(m, k, v reflect.Value) error {
|
|
return nil
|
|
}
|
|
func decodeInterface(expr hcl.Expression, ctx *hcl.EvalContext) (interface{}, hcl.Diagnostics) {
|
|
srvVal, diags := expr.Value(ctx)
|
|
|
|
dst, err := interfaceFromCtyValue(srvVal)
|
|
if err != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "unsuitable value type",
|
|
Detail: fmt.Sprintf("Unsuitable value: %s", err.Error()),
|
|
Subject: expr.StartRange().Ptr(),
|
|
Context: expr.Range().Ptr(),
|
|
})
|
|
}
|
|
|
|
return dst, diags
|
|
}
|
|
|
|
func interfaceFromCtyValue(val cty.Value) (interface{}, error) {
|
|
t := val.Type()
|
|
|
|
if val.IsNull() {
|
|
return nil, nil
|
|
}
|
|
|
|
if !val.IsKnown() {
|
|
return nil, fmt.Errorf("value is not known")
|
|
}
|
|
|
|
// The caller should've guaranteed that the given val is conformant with
|
|
// the given type t, so we'll proceed under that assumption here.
|
|
|
|
switch {
|
|
case t.IsPrimitiveType():
|
|
switch t {
|
|
case cty.String:
|
|
return val.AsString(), nil
|
|
case cty.Number:
|
|
if val.RawEquals(cty.PositiveInfinity) {
|
|
return math.Inf(1), nil
|
|
} else if val.RawEquals(cty.NegativeInfinity) {
|
|
return math.Inf(-1), nil
|
|
} else {
|
|
return smallestNumber(val.AsBigFloat()), nil
|
|
}
|
|
case cty.Bool:
|
|
return val.True(), nil
|
|
default:
|
|
panic("unsupported primitive type")
|
|
}
|
|
case isCollectionOfMaps(t):
|
|
result := []map[string]interface{}{}
|
|
|
|
it := val.ElementIterator()
|
|
for it.Next() {
|
|
_, ev := it.Element()
|
|
evi, err := interfaceFromCtyValue(ev)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, evi.(map[string]interface{}))
|
|
}
|
|
return result, nil
|
|
case t.IsListType(), t.IsSetType(), t.IsTupleType():
|
|
result := []interface{}{}
|
|
|
|
it := val.ElementIterator()
|
|
for it.Next() {
|
|
_, ev := it.Element()
|
|
evi, err := interfaceFromCtyValue(ev)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, evi)
|
|
}
|
|
return result, nil
|
|
case t.IsMapType():
|
|
result := map[string]interface{}{}
|
|
it := val.ElementIterator()
|
|
for it.Next() {
|
|
ek, ev := it.Element()
|
|
|
|
ekv := ek.AsString()
|
|
evv, err := interfaceFromCtyValue(ev)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result[ekv] = evv
|
|
}
|
|
return result, nil
|
|
case t.IsObjectType():
|
|
result := map[string]interface{}{}
|
|
|
|
for k := range t.AttributeTypes() {
|
|
av := val.GetAttr(k)
|
|
avv, err := interfaceFromCtyValue(av)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result[k] = avv
|
|
}
|
|
return result, nil
|
|
case t.IsCapsuleType():
|
|
rawVal := val.EncapsulatedValue()
|
|
return rawVal, nil
|
|
default:
|
|
// should never happen
|
|
return nil, fmt.Errorf("cannot serialize %s", t.FriendlyName())
|
|
}
|
|
}
|
|
|
|
func isCollectionOfMaps(t cty.Type) bool {
|
|
switch {
|
|
case t.IsCollectionType():
|
|
et := t.ElementType()
|
|
return et.IsMapType() || et.IsObjectType()
|
|
case t.IsTupleType():
|
|
ets := t.TupleElementTypes()
|
|
for _, et := range ets {
|
|
if !et.IsMapType() && !et.IsObjectType() {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return len(ets) > 0
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func smallestNumber(b *big.Float) interface{} {
|
|
if v, acc := b.Int64(); acc == big.Exact {
|
|
// check if it fits in int
|
|
if int64(int(v)) == v {
|
|
return int(v)
|
|
}
|
|
return v
|
|
}
|
|
|
|
v, _ := b.Float64()
|
|
return v
|
|
}
|