package hclhil import ( "fmt" "strconv" "strings" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/function" "github.com/zclconf/go-zcl/zcl" "github.com/hashicorp/hil" hilast "github.com/hashicorp/hil/ast" ) func parseTemplate(src []byte, filename string, startPos zcl.Pos) (zcl.Expression, zcl.Diagnostics) { hilStartPos := hilast.Pos{ Filename: filename, Line: startPos.Line, Column: startPos.Column, // HIL positions don't have byte offsets, so we ignore startPos.Byte here } rootNode, err := hil.ParseWithPosition(string(src), hilStartPos) if err != nil { return nil, zcl.Diagnostics{ { Severity: zcl.DiagError, Summary: "Syntax error in template", Detail: fmt.Sprintf("The template could not be parsed: %s", err), Subject: errorRange(err), }, } } return &templateExpression{ node: rootNode, }, nil } type templateExpression struct { node hilast.Node } func (e *templateExpression) Value(ctx *zcl.EvalContext) (cty.Value, zcl.Diagnostics) { cfg := hilEvalConfig(ctx) return ctyValueFromHILNode(e.node, cfg) } func (e *templateExpression) Variables() []zcl.Traversal { var vars []zcl.Traversal e.node.Accept(func(n hilast.Node) hilast.Node { vn, ok := n.(*hilast.VariableAccess) if !ok { return n } rawName := vn.Name parts := strings.Split(rawName, ".") if len(parts) == 0 { return n } tr := make(zcl.Traversal, 0, len(parts)) tr = append(tr, zcl.TraverseRoot{ Name: parts[0], SrcRange: rangeFromHILPos(n.Pos()), }) for _, name := range parts { if nv, err := strconv.Atoi(name); err == nil { // Turn this into a sequence index in zcl land, to save // callers from having to understand both HIL-style numeric // attributes and zcl-style indices. tr = append(tr, zcl.TraverseIndex{ Key: cty.NumberIntVal(int64(nv)), SrcRange: rangeFromHILPos(n.Pos()), }) continue } if name == "*" { // TODO: support splat traversals, but that requires some // more work here because we need to then accumulate the // rest of the parts into the splat's own "Each" traversal. continue } tr = append(tr, zcl.TraverseAttr{ Name: name, SrcRange: rangeFromHILPos(n.Pos()), }) } vars = append(vars, tr) return n }) return vars } func (e *templateExpression) Range() zcl.Range { return rangeFromHILPos(e.node.Pos()) } func (e *templateExpression) StartRange() zcl.Range { return rangeFromHILPos(e.node.Pos()) } func hilEvalConfig(ctx *zcl.EvalContext) *hil.EvalConfig { cfg := &hil.EvalConfig{ GlobalScope: &hilast.BasicScope{ VarMap: map[string]hilast.Variable{}, FuncMap: map[string]hilast.Function{}, }, } if ctx == nil { return cfg } if ctx.Variables != nil { for name, val := range ctx.Variables { cfg.GlobalScope.VarMap[name] = hilVariableForInput(hilVariableFromCtyValue(val)) } } if ctx.Functions != nil { for name, hf := range ctx.Functions { cfg.GlobalScope.FuncMap[name] = hilFunctionFromCtyFunction(hf) } } return cfg } func ctyValueFromHILNode(node hilast.Node, cfg *hil.EvalConfig) (cty.Value, zcl.Diagnostics) { result, err := hil.Eval(node, cfg) if err != nil { return cty.DynamicVal, zcl.Diagnostics{ { Severity: zcl.DiagError, Summary: "Template evaluation failed", Detail: fmt.Sprintf("Error while evaluating template: %s", err), Subject: rangeFromHILPos(node.Pos()).Ptr(), }, } } return ctyValueFromHILResult(result), nil } func ctyValueFromHILResult(result hil.EvaluationResult) cty.Value { switch result.Type { case hil.TypeString: return cty.StringVal(result.Value.(string)) case hil.TypeBool: return cty.BoolVal(result.Value.(bool)) case hil.TypeList: varsI := result.Value.([]interface{}) if len(varsI) == 0 { return cty.ListValEmpty(cty.String) } vals := make([]cty.Value, len(varsI)) for i, varI := range varsI { hv, err := hil.InterfaceToVariable(varI) if err != nil { panic("HIL returned type that can't be converted back to variable") } vals[i] = ctyValueFromHILVariable(hv) } return cty.TupleVal(vals) case hil.TypeMap: varsI := result.Value.(map[string]interface{}) if len(varsI) == 0 { return cty.MapValEmpty(cty.String) } vals := make(map[string]cty.Value) for key, varI := range varsI { hv, err := hil.InterfaceToVariable(varI) if err != nil { panic("HIL returned type that can't be converted back to variable") } vals[key] = ctyValueFromHILVariable(hv) } return cty.ObjectVal(vals) case hil.TypeUnknown: // HIL doesn't have typed unknowns, so we have to return dynamic return cty.DynamicVal default: // should never happen panic(fmt.Sprintf("unsupported EvaluationResult type %s", result.Type)) } } func ctyValueFromHILVariable(vr hilast.Variable) cty.Value { switch vr.Type { case hilast.TypeBool: return cty.BoolVal(vr.Value.(bool)) case hilast.TypeString: return cty.StringVal(vr.Value.(string)) case hilast.TypeInt: return cty.NumberIntVal(vr.Value.(int64)) case hilast.TypeFloat: return cty.NumberFloatVal(vr.Value.(float64)) case hilast.TypeList: vars := vr.Value.([]hilast.Variable) if len(vars) == 0 { return cty.ListValEmpty(cty.String) } vals := make([]cty.Value, len(vars)) for i, v := range vars { vals[i] = ctyValueFromHILVariable(v) } return cty.TupleVal(vals) case hilast.TypeMap: vars := vr.Value.(map[string]hilast.Variable) if len(vars) == 0 { return cty.MapValEmpty(cty.String) } vals := make(map[string]cty.Value) for key, v := range vars { vals[key] = ctyValueFromHILVariable(v) } return cty.ObjectVal(vals) case hilast.TypeAny, hilast.TypeUnknown: return cty.DynamicVal default: // should never happen panic(fmt.Sprintf("unsupported HIL Variable type %s", vr.Type)) } } func hilVariableFromCtyValue(val cty.Value) hilast.Variable { if !val.IsKnown() { return hilast.Variable{ Type: hilast.TypeUnknown, Value: hil.UnknownValue, } } if val.IsNull() { // HIL doesn't actually support nulls, so we'll cheat a bit and // use an unknown. This is not quite right since nulls are supposed // to fail when evaluated, but it should suffice as a compatibility // shim since HIL-using applications probably won't be generating // nulls anyway. return hilast.Variable{ Type: hilast.TypeUnknown, Value: hil.UnknownValue, } } ty := val.Type() switch ty { case cty.String: return hilast.Variable{ Type: hilast.TypeString, Value: val.AsString(), } case cty.Number: // cty doesn't distinguish between floats and ints, so we'll // just always use floats here and depend on automatic conversions // to produce ints where needed. bf := val.AsBigFloat() f, _ := bf.Float64() return hilast.Variable{ Type: hilast.TypeFloat, Value: f, } case cty.Bool: return hilast.Variable{ Type: hilast.TypeBool, Value: val.True(), } } switch { case ty.IsListType() || ty.IsSetType() || ty.IsTupleType(): // HIL doesn't have sets, so we'll just turn them into lists // HIL doesn't support tuples either, so any tuples without consistent // element types will fail HIL's check for consistent types, but that's // okay since we don't intend to change HIL semantics here. vars := []hilast.Variable{} it := val.ElementIterator() for it.Next() { _, ev := it.Element() vars = append(vars, hilVariableFromCtyValue(ev)) } return hilast.Variable{ Type: hilast.TypeList, Value: vars, } case ty.IsMapType(): vars := map[string]hilast.Variable{} it := val.ElementIterator() for it.Next() { kv, ev := it.Element() k := kv.AsString() vars[k] = hilVariableFromCtyValue(ev) } return hilast.Variable{ Type: hilast.TypeMap, Value: vars, } case ty.IsObjectType(): // HIL doesn't support objects, so objects that don't have consistent // attribute types will fail HIL's check for consistent types. That's // okay since we don't intend to change HIL semantics here. vars := map[string]interface{}{} atys := ty.AttributeTypes() for k := range atys { vars[k] = hilVariableFromCtyValue(val.GetAttr(k)) } return hilast.Variable{ Type: hilast.TypeMap, Value: vars, } case ty.IsCapsuleType(): // Can't do anything reasonable with capsule types, so we'll just // treat them as unknown and let the caller deal with it as an error. return hilast.Variable{ Type: hilast.TypeUnknown, Value: hil.UnknownValue, } default: // Should never happen if we've done our job right here panic(fmt.Sprintf("don't know how to convert %#v into a HIL variable", ty)) } } // hilVariableForInput constrains the given variable to be of the types HIL // accepts as input, which entails converting all primitive types to string. func hilVariableForInput(v hilast.Variable) hilast.Variable { switch v.Type { case hilast.TypeFloat: return hilast.Variable{ Type: hilast.TypeString, Value: strconv.FormatFloat(v.Value.(float64), 'f', -1, 64), } case hilast.TypeBool: if v.Value.(bool) { return hilast.Variable{ Type: hilast.TypeString, Value: "true", } } else { return hilast.Variable{ Type: hilast.TypeString, Value: "false", } } case hilast.TypeList: inVars := v.Value.([]hilast.Variable) outVars := make([]hilast.Variable, len(inVars)) for i, inVar := range inVars { outVars[i] = hilVariableForInput(inVar) } return hilast.Variable{ Type: hilast.TypeList, Value: outVars, } case hilast.TypeMap: inVars := v.Value.(map[string]hilast.Variable) outVars := make(map[string]hilast.Variable) for k, inVar := range inVars { outVars[k] = hilVariableForInput(inVar) } return hilast.Variable{ Type: hilast.TypeMap, Value: outVars, } default: return v } } func hilTypeFromCtyType(ty cty.Type) hilast.Type { switch ty { case cty.String: return hilast.TypeString case cty.Number: return hilast.TypeFloat case cty.Bool: return hilast.TypeBool case cty.DynamicPseudoType: // Assume we're using this as a type specification, so we'd rather // have TypeAny than TypeUnknown. return hilast.TypeAny } switch { case ty.IsListType() || ty.IsSetType() || ty.IsTupleType(): return hilast.TypeList case ty.IsMapType(), ty.IsObjectType(): return hilast.TypeMap default: return hilast.TypeUnknown } } func hilFunctionFromCtyFunction(f function.Function) hilast.Function { hf := hilast.Function{} params := f.Params() varParam := f.VarParam() hf.ArgTypes = make([]hilast.Type, len(params)) staticTypes := make([]cty.Type, len(params)) for i, param := range params { hf.ArgTypes[i] = hilTypeFromCtyType(param.Type) staticTypes[i] = param.Type } if varParam != nil { hf.Variadic = true hf.VariadicType = hilTypeFromCtyType(varParam.Type) } retType, err := f.ReturnType(staticTypes) if err == nil { hf.ReturnType = hilTypeFromCtyType(retType) } else { hf.ReturnType = hilTypeFromCtyType(cty.DynamicPseudoType) } hf.Callback = func(hilArgs []interface{}) (interface{}, error) { args := make([]cty.Value, len(hilArgs)) for i, hilArg := range hilArgs { var hilType hilast.Type if i < len(hf.ArgTypes) { hilType = hf.ArgTypes[i] } else { hilType = hf.VariadicType } args[i] = ctyValueFromHILVariable(hilast.Variable{ Type: hilType, Value: hilArg, }) } result, err := f.Call(args) if err != nil { return nil, err } hilResult := hilVariableFromCtyValue(result) return hilResult.Value, nil } return hf }