github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/configs/hcl2shim/values.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package hcl2shim 5 6 import ( 7 "fmt" 8 "math/big" 9 10 "github.com/zclconf/go-cty/cty" 11 12 "github.com/terramate-io/tf/configs/configschema" 13 ) 14 15 // UnknownVariableValue is a sentinel value that can be used 16 // to denote that the value of a variable is unknown at this time. 17 // RawConfig uses this information to build up data about 18 // unknown keys. 19 const UnknownVariableValue = "74D93920-ED26-11E3-AC10-0800200C9A66" 20 21 // ConfigValueFromHCL2Block is like ConfigValueFromHCL2 but it works only for 22 // known object values and uses the provided block schema to perform some 23 // additional normalization to better mimic the shape of value that the old 24 // HCL1/HIL-based codepaths would've produced. 25 // 26 // In particular, it discards the collections that we use to represent nested 27 // blocks (other than NestingSingle) if they are empty, which better mimics 28 // the HCL1 behavior because HCL1 had no knowledge of the schema and so didn't 29 // know that an unspecified block _could_ exist. 30 // 31 // The given object value must conform to the schema's implied type or this 32 // function will panic or produce incorrect results. 33 // 34 // This is primarily useful for the final transition from new-style values to 35 // terraform.ResourceConfig before calling to a legacy provider, since 36 // helper/schema (the old provider SDK) is particularly sensitive to these 37 // subtle differences within its validation code. 38 func ConfigValueFromHCL2Block(v cty.Value, schema *configschema.Block) map[string]interface{} { 39 if v.IsNull() { 40 return nil 41 } 42 if !v.IsKnown() { 43 panic("ConfigValueFromHCL2Block used with unknown value") 44 } 45 if !v.Type().IsObjectType() { 46 panic(fmt.Sprintf("ConfigValueFromHCL2Block used with non-object value %#v", v)) 47 } 48 49 atys := v.Type().AttributeTypes() 50 ret := make(map[string]interface{}) 51 52 for name := range schema.Attributes { 53 if _, exists := atys[name]; !exists { 54 continue 55 } 56 57 av := v.GetAttr(name) 58 if av.IsNull() { 59 // Skip nulls altogether, to better mimic how HCL1 would behave 60 continue 61 } 62 ret[name] = ConfigValueFromHCL2(av) 63 } 64 65 for name, blockS := range schema.BlockTypes { 66 if _, exists := atys[name]; !exists { 67 continue 68 } 69 bv := v.GetAttr(name) 70 if !bv.IsKnown() { 71 ret[name] = UnknownVariableValue 72 continue 73 } 74 if bv.IsNull() { 75 continue 76 } 77 78 switch blockS.Nesting { 79 80 case configschema.NestingSingle, configschema.NestingGroup: 81 ret[name] = ConfigValueFromHCL2Block(bv, &blockS.Block) 82 83 case configschema.NestingList, configschema.NestingSet: 84 l := bv.LengthInt() 85 if l == 0 { 86 // skip empty collections to better mimic how HCL1 would behave 87 continue 88 } 89 90 elems := make([]interface{}, 0, l) 91 for it := bv.ElementIterator(); it.Next(); { 92 _, ev := it.Element() 93 if !ev.IsKnown() { 94 elems = append(elems, UnknownVariableValue) 95 continue 96 } 97 elems = append(elems, ConfigValueFromHCL2Block(ev, &blockS.Block)) 98 } 99 ret[name] = elems 100 101 case configschema.NestingMap: 102 if bv.LengthInt() == 0 { 103 // skip empty collections to better mimic how HCL1 would behave 104 continue 105 } 106 107 elems := make(map[string]interface{}) 108 for it := bv.ElementIterator(); it.Next(); { 109 ek, ev := it.Element() 110 if !ev.IsKnown() { 111 elems[ek.AsString()] = UnknownVariableValue 112 continue 113 } 114 elems[ek.AsString()] = ConfigValueFromHCL2Block(ev, &blockS.Block) 115 } 116 ret[name] = elems 117 } 118 } 119 120 return ret 121 } 122 123 // ConfigValueFromHCL2 converts a value from HCL2 (really, from the cty dynamic 124 // types library that HCL2 uses) to a value type that matches what would've 125 // been produced from the HCL-based interpolator for an equivalent structure. 126 // 127 // This function will transform a cty null value into a Go nil value, which 128 // isn't a possible outcome of the HCL/HIL-based decoder and so callers may 129 // need to detect and reject any null values. 130 func ConfigValueFromHCL2(v cty.Value) interface{} { 131 if !v.IsKnown() { 132 return UnknownVariableValue 133 } 134 if v.IsNull() { 135 return nil 136 } 137 138 switch v.Type() { 139 case cty.Bool: 140 return v.True() // like HCL.BOOL 141 case cty.String: 142 return v.AsString() // like HCL token.STRING or token.HEREDOC 143 case cty.Number: 144 // We can't match HCL _exactly_ here because it distinguishes between 145 // int and float values, but we'll get as close as we can by using 146 // an int if the number is exactly representable, and a float if not. 147 // The conversion to float will force precision to that of a float64, 148 // which is potentially losing information from the specific number 149 // given, but no worse than what HCL would've done in its own conversion 150 // to float. 151 152 f := v.AsBigFloat() 153 if i, acc := f.Int64(); acc == big.Exact { 154 // if we're on a 32-bit system and the number is too big for 32-bit 155 // int then we'll fall through here and use a float64. 156 const MaxInt = int(^uint(0) >> 1) 157 const MinInt = -MaxInt - 1 158 if i <= int64(MaxInt) && i >= int64(MinInt) { 159 return int(i) // Like HCL token.NUMBER 160 } 161 } 162 163 f64, _ := f.Float64() 164 return f64 // like HCL token.FLOAT 165 } 166 167 if v.Type().IsListType() || v.Type().IsSetType() || v.Type().IsTupleType() { 168 l := make([]interface{}, 0, v.LengthInt()) 169 it := v.ElementIterator() 170 for it.Next() { 171 _, ev := it.Element() 172 l = append(l, ConfigValueFromHCL2(ev)) 173 } 174 return l 175 } 176 177 if v.Type().IsMapType() || v.Type().IsObjectType() { 178 l := make(map[string]interface{}) 179 it := v.ElementIterator() 180 for it.Next() { 181 ek, ev := it.Element() 182 cv := ConfigValueFromHCL2(ev) 183 if cv != nil { 184 l[ek.AsString()] = cv 185 } 186 } 187 return l 188 } 189 190 // If we fall out here then we have some weird type that we haven't 191 // accounted for. This should never happen unless the caller is using 192 // capsule types, and we don't currently have any such types defined. 193 panic(fmt.Errorf("can't convert %#v to config value", v)) 194 } 195 196 // HCL2ValueFromConfigValue is the opposite of configValueFromHCL2: it takes 197 // a value as would be returned from the old interpolator and turns it into 198 // a cty.Value so it can be used within, for example, an HCL2 EvalContext. 199 func HCL2ValueFromConfigValue(v interface{}) cty.Value { 200 if v == nil { 201 return cty.NullVal(cty.DynamicPseudoType) 202 } 203 if v == UnknownVariableValue { 204 return cty.DynamicVal 205 } 206 207 switch tv := v.(type) { 208 case bool: 209 return cty.BoolVal(tv) 210 case string: 211 return cty.StringVal(tv) 212 case int: 213 return cty.NumberIntVal(int64(tv)) 214 case float64: 215 return cty.NumberFloatVal(tv) 216 case []interface{}: 217 vals := make([]cty.Value, len(tv)) 218 for i, ev := range tv { 219 vals[i] = HCL2ValueFromConfigValue(ev) 220 } 221 return cty.TupleVal(vals) 222 case map[string]interface{}: 223 vals := map[string]cty.Value{} 224 for k, ev := range tv { 225 vals[k] = HCL2ValueFromConfigValue(ev) 226 } 227 return cty.ObjectVal(vals) 228 default: 229 // HCL/HIL should never generate anything that isn't caught by 230 // the above, so if we get here something has gone very wrong. 231 panic(fmt.Errorf("can't convert %#v to cty.Value", v)) 232 } 233 }