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