package hcl2shim import ( "fmt" "math/big" "github.com/hashicorp/hil/ast" "github.com/zclconf/go-cty/cty" ) // UnknownVariableValue is a sentinel value that can be used // to denote that the value of a variable is unknown at this time. // RawConfig uses this information to build up data about // unknown keys. const UnknownVariableValue = "74D93920-ED26-11E3-AC10-0800200C9A66" // ConfigValueFromHCL2 converts a value from HCL2 (really, from the cty dynamic // types library that HCL2 uses) to a value type that matches what would've // been produced from the HCL-based interpolator for an equivalent structure. // // This function will transform a cty null value into a Go nil value, which // isn't a possible outcome of the HCL/HIL-based decoder and so callers may // need to detect and reject any null values. func ConfigValueFromHCL2(v cty.Value) interface{} { if !v.IsKnown() { return UnknownVariableValue } if v.IsNull() { return nil } switch v.Type() { case cty.Bool: return v.True() // like HCL.BOOL case cty.String: return v.AsString() // like HCL token.STRING or token.HEREDOC case cty.Number: // We can't match HCL _exactly_ here because it distinguishes between // int and float values, but we'll get as close as we can by using // an int if the number is exactly representable, and a float if not. // The conversion to float will force precision to that of a float64, // which is potentially losing information from the specific number // given, but no worse than what HCL would've done in its own conversion // to float. f := v.AsBigFloat() if i, acc := f.Int64(); acc == big.Exact { // if we're on a 32-bit system and the number is too big for 32-bit // int then we'll fall through here and use a float64. const MaxInt = int(^uint(0) >> 1) const MinInt = -MaxInt - 1 if i <= int64(MaxInt) && i >= int64(MinInt) { return int(i) // Like HCL token.NUMBER } } f64, _ := f.Float64() return f64 // like HCL token.FLOAT } if v.Type().IsListType() || v.Type().IsSetType() || v.Type().IsTupleType() { l := make([]interface{}, 0, v.LengthInt()) it := v.ElementIterator() for it.Next() { _, ev := it.Element() l = append(l, ConfigValueFromHCL2(ev)) } return l } if v.Type().IsMapType() || v.Type().IsObjectType() { l := make(map[string]interface{}) it := v.ElementIterator() for it.Next() { ek, ev := it.Element() l[ek.AsString()] = ConfigValueFromHCL2(ev) } return l } // If we fall out here then we have some weird type that we haven't // accounted for. This should never happen unless the caller is using // capsule types, and we don't currently have any such types defined. panic(fmt.Errorf("can't convert %#v to config value", v)) } // HCL2ValueFromConfigValue is the opposite of configValueFromHCL2: it takes // a value as would be returned from the old interpolator and turns it into // a cty.Value so it can be used within, for example, an HCL2 EvalContext. func HCL2ValueFromConfigValue(v interface{}) cty.Value { if v == nil { return cty.NullVal(cty.DynamicPseudoType) } if v == UnknownVariableValue { return cty.DynamicVal } switch tv := v.(type) { case bool: return cty.BoolVal(tv) case string: return cty.StringVal(tv) case int: return cty.NumberIntVal(int64(tv)) case float64: return cty.NumberFloatVal(tv) case []interface{}: vals := make([]cty.Value, len(tv)) for i, ev := range tv { vals[i] = HCL2ValueFromConfigValue(ev) } return cty.TupleVal(vals) case map[string]interface{}: vals := map[string]cty.Value{} for k, ev := range tv { vals[k] = HCL2ValueFromConfigValue(ev) } return cty.ObjectVal(vals) default: // HCL/HIL should never generate anything that isn't caught by // the above, so if we get here something has gone very wrong. panic(fmt.Errorf("can't convert %#v to cty.Value", v)) } } func HILVariableFromHCL2Value(v cty.Value) ast.Variable { if v.IsNull() { // Caller should guarantee/check this before calling panic("Null values cannot be represented in HIL") } if !v.IsKnown() { return ast.Variable{ Type: ast.TypeUnknown, Value: UnknownVariableValue, } } switch v.Type() { case cty.Bool: return ast.Variable{ Type: ast.TypeBool, Value: v.True(), } case cty.Number: v := ConfigValueFromHCL2(v) switch tv := v.(type) { case int: return ast.Variable{ Type: ast.TypeInt, Value: tv, } case float64: return ast.Variable{ Type: ast.TypeFloat, Value: tv, } default: // should never happen panic("invalid return value for configValueFromHCL2") } case cty.String: return ast.Variable{ Type: ast.TypeString, Value: v.AsString(), } } if v.Type().IsListType() || v.Type().IsSetType() || v.Type().IsTupleType() { l := make([]ast.Variable, 0, v.LengthInt()) it := v.ElementIterator() for it.Next() { _, ev := it.Element() l = append(l, HILVariableFromHCL2Value(ev)) } // If we were given a tuple then this could actually produce an invalid // list with non-homogenous types, which we expect to be caught inside // HIL just like a user-supplied non-homogenous list would be. return ast.Variable{ Type: ast.TypeList, Value: l, } } if v.Type().IsMapType() || v.Type().IsObjectType() { l := make(map[string]ast.Variable) it := v.ElementIterator() for it.Next() { ek, ev := it.Element() l[ek.AsString()] = HILVariableFromHCL2Value(ev) } // If we were given an object then this could actually produce an invalid // map with non-homogenous types, which we expect to be caught inside // HIL just like a user-supplied non-homogenous map would be. return ast.Variable{ Type: ast.TypeMap, Value: l, } } // If we fall out here then we have some weird type that we haven't // accounted for. This should never happen unless the caller is using // capsule types, and we don't currently have any such types defined. panic(fmt.Errorf("can't convert %#v to HIL variable", v)) } func HCL2ValueFromHILVariable(v ast.Variable) cty.Value { switch v.Type { case ast.TypeList: vals := make([]cty.Value, len(v.Value.([]ast.Variable))) for i, ev := range v.Value.([]ast.Variable) { vals[i] = HCL2ValueFromHILVariable(ev) } return cty.TupleVal(vals) case ast.TypeMap: vals := make(map[string]cty.Value, len(v.Value.(map[string]ast.Variable))) for k, ev := range v.Value.(map[string]ast.Variable) { vals[k] = HCL2ValueFromHILVariable(ev) } return cty.ObjectVal(vals) default: return HCL2ValueFromConfigValue(v.Value) } } func HCL2TypeForHILType(hilType ast.Type) cty.Type { switch hilType { case ast.TypeAny: return cty.DynamicPseudoType case ast.TypeUnknown: return cty.DynamicPseudoType case ast.TypeBool: return cty.Bool case ast.TypeInt: return cty.Number case ast.TypeFloat: return cty.Number case ast.TypeString: return cty.String case ast.TypeList: return cty.List(cty.DynamicPseudoType) case ast.TypeMap: return cty.Map(cty.DynamicPseudoType) default: return cty.NilType // equilvalent to ast.TypeInvalid } }