github.com/opentofu/opentofu@v1.7.1/internal/configs/hcl2shim/flatmap.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 "strconv" 11 "strings" 12 13 "github.com/zclconf/go-cty/cty/convert" 14 15 "github.com/zclconf/go-cty/cty" 16 ) 17 18 // FlatmapValueFromHCL2 converts a value from HCL2 (really, from the cty dynamic 19 // types library that HCL2 uses) to a map compatible with what would be 20 // produced by the "flatmap" package. 21 // 22 // The type of the given value informs the structure of the resulting map. 23 // The value must be of an object type or this function will panic. 24 // 25 // Flatmap values can only represent maps when they are of primitive types, 26 // so the given value must not have any maps of complex types or the result 27 // is undefined. 28 func FlatmapValueFromHCL2(v cty.Value) map[string]string { 29 if v.IsNull() { 30 return nil 31 } 32 33 if !v.Type().IsObjectType() { 34 panic(fmt.Sprintf("HCL2ValueFromFlatmap called on %#v", v.Type())) 35 } 36 37 m := make(map[string]string) 38 flatmapValueFromHCL2Map(m, "", v) 39 return m 40 } 41 42 func flatmapValueFromHCL2Value(m map[string]string, key string, val cty.Value) { 43 ty := val.Type() 44 switch { 45 case ty.IsPrimitiveType() || ty == cty.DynamicPseudoType: 46 flatmapValueFromHCL2Primitive(m, key, val) 47 case ty.IsObjectType() || ty.IsMapType(): 48 flatmapValueFromHCL2Map(m, key+".", val) 49 case ty.IsTupleType() || ty.IsListType() || ty.IsSetType(): 50 flatmapValueFromHCL2Seq(m, key+".", val) 51 default: 52 panic(fmt.Sprintf("cannot encode %s to flatmap", ty.FriendlyName())) 53 } 54 } 55 56 func flatmapValueFromHCL2Primitive(m map[string]string, key string, val cty.Value) { 57 if !val.IsKnown() { 58 m[key] = UnknownVariableValue 59 return 60 } 61 if val.IsNull() { 62 // Omit entirely 63 return 64 } 65 66 var err error 67 val, err = convert.Convert(val, cty.String) 68 if err != nil { 69 // Should not be possible, since all primitive types can convert to string. 70 panic(fmt.Sprintf("invalid primitive encoding to flatmap: %s", err)) 71 } 72 m[key] = val.AsString() 73 } 74 75 func flatmapValueFromHCL2Map(m map[string]string, prefix string, val cty.Value) { 76 if val.IsNull() { 77 // Omit entirely 78 return 79 } 80 if !val.IsKnown() { 81 switch { 82 case val.Type().IsObjectType(): 83 // Whole objects can't be unknown in flatmap, so instead we'll 84 // just write all of the attribute values out as unknown. 85 for name, aty := range val.Type().AttributeTypes() { 86 flatmapValueFromHCL2Value(m, prefix+name, cty.UnknownVal(aty)) 87 } 88 default: 89 m[prefix+"%"] = UnknownVariableValue 90 } 91 return 92 } 93 94 len := 0 95 for it := val.ElementIterator(); it.Next(); { 96 ak, av := it.Element() 97 name := ak.AsString() 98 flatmapValueFromHCL2Value(m, prefix+name, av) 99 len++ 100 } 101 if !val.Type().IsObjectType() { // objects don't have an explicit count included, since their attribute count is fixed 102 m[prefix+"%"] = strconv.Itoa(len) 103 } 104 } 105 106 func flatmapValueFromHCL2Seq(m map[string]string, prefix string, val cty.Value) { 107 if val.IsNull() { 108 // Omit entirely 109 return 110 } 111 if !val.IsKnown() { 112 m[prefix+"#"] = UnknownVariableValue 113 return 114 } 115 116 // For sets this won't actually generate exactly what helper/schema would've 117 // generated, because we don't have access to the set key function it 118 // would've used. However, in practice it doesn't actually matter what the 119 // keys are as long as they are unique, so we'll just generate sequential 120 // indexes for them as if it were a list. 121 // 122 // An important implication of this, however, is that the set ordering will 123 // not be consistent across mutations and so different keys may be assigned 124 // to the same value when round-tripping. Since this shim is intended to 125 // be short-lived and not used for round-tripping, we accept this. 126 i := 0 127 for it := val.ElementIterator(); it.Next(); { 128 _, av := it.Element() 129 key := prefix + strconv.Itoa(i) 130 flatmapValueFromHCL2Value(m, key, av) 131 i++ 132 } 133 m[prefix+"#"] = strconv.Itoa(i) 134 } 135 136 // HCL2ValueFromFlatmap converts a map compatible with what would be produced 137 // by the "flatmap" package to a HCL2 (really, the cty dynamic types library 138 // that HCL2 uses) object type. 139 // 140 // The intended result type must be provided in order to guide how the 141 // map contents are decoded. This must be an object type or this function 142 // will panic. 143 // 144 // Flatmap values can only represent maps when they are of primitive types, 145 // so the given type must not have any maps of complex types or the result 146 // is undefined. 147 // 148 // The result may contain null values if the given map does not contain keys 149 // for all of the different key paths implied by the given type. 150 func HCL2ValueFromFlatmap(m map[string]string, ty cty.Type) (cty.Value, error) { 151 if m == nil { 152 return cty.NullVal(ty), nil 153 } 154 if !ty.IsObjectType() { 155 panic(fmt.Sprintf("HCL2ValueFromFlatmap called on %#v", ty)) 156 } 157 158 return hcl2ValueFromFlatmapObject(m, "", ty.AttributeTypes()) 159 } 160 161 func hcl2ValueFromFlatmapValue(m map[string]string, key string, ty cty.Type) (cty.Value, error) { 162 var val cty.Value 163 var err error 164 switch { 165 case ty.IsPrimitiveType(): 166 val, err = hcl2ValueFromFlatmapPrimitive(m, key, ty) 167 case ty.IsObjectType(): 168 val, err = hcl2ValueFromFlatmapObject(m, key+".", ty.AttributeTypes()) 169 case ty.IsTupleType(): 170 val, err = hcl2ValueFromFlatmapTuple(m, key+".", ty.TupleElementTypes()) 171 case ty.IsMapType(): 172 val, err = hcl2ValueFromFlatmapMap(m, key+".", ty) 173 case ty.IsListType(): 174 val, err = hcl2ValueFromFlatmapList(m, key+".", ty) 175 case ty.IsSetType(): 176 val, err = hcl2ValueFromFlatmapSet(m, key+".", ty) 177 default: 178 err = fmt.Errorf("cannot decode %s from flatmap", ty.FriendlyName()) 179 } 180 181 if err != nil { 182 return cty.DynamicVal, err 183 } 184 return val, nil 185 } 186 187 func hcl2ValueFromFlatmapPrimitive(m map[string]string, key string, ty cty.Type) (cty.Value, error) { 188 rawVal, exists := m[key] 189 if !exists { 190 return cty.NullVal(ty), nil 191 } 192 if rawVal == UnknownVariableValue { 193 return cty.UnknownVal(ty), nil 194 } 195 196 var err error 197 val := cty.StringVal(rawVal) 198 val, err = convert.Convert(val, ty) 199 if err != nil { 200 // This should never happen for _valid_ input, but flatmap data might 201 // be tampered with by the user and become invalid. 202 return cty.DynamicVal, fmt.Errorf("invalid value for %q in state: %w", key, err) 203 } 204 205 return val, nil 206 } 207 208 func hcl2ValueFromFlatmapObject(m map[string]string, prefix string, atys map[string]cty.Type) (cty.Value, error) { 209 vals := make(map[string]cty.Value) 210 for name, aty := range atys { 211 val, err := hcl2ValueFromFlatmapValue(m, prefix+name, aty) 212 if err != nil { 213 return cty.DynamicVal, err 214 } 215 vals[name] = val 216 } 217 return cty.ObjectVal(vals), nil 218 } 219 220 func hcl2ValueFromFlatmapTuple(m map[string]string, prefix string, etys []cty.Type) (cty.Value, error) { 221 var vals []cty.Value 222 223 // if the container is unknown, there is no count string 224 listName := strings.TrimRight(prefix, ".") 225 if m[listName] == UnknownVariableValue { 226 return cty.UnknownVal(cty.Tuple(etys)), nil 227 } 228 229 countStr, exists := m[prefix+"#"] 230 if !exists { 231 return cty.NullVal(cty.Tuple(etys)), nil 232 } 233 if countStr == UnknownVariableValue { 234 return cty.UnknownVal(cty.Tuple(etys)), nil 235 } 236 237 count, err := strconv.Atoi(countStr) 238 if err != nil { 239 return cty.DynamicVal, fmt.Errorf("invalid count value for %q in state: %w", prefix, err) 240 } 241 if count != len(etys) { 242 return cty.DynamicVal, fmt.Errorf("wrong number of values for %q in state: got %d, but need %d", prefix, count, len(etys)) 243 } 244 245 vals = make([]cty.Value, len(etys)) 246 for i, ety := range etys { 247 key := prefix + strconv.Itoa(i) 248 val, err := hcl2ValueFromFlatmapValue(m, key, ety) 249 if err != nil { 250 return cty.DynamicVal, err 251 } 252 vals[i] = val 253 } 254 return cty.TupleVal(vals), nil 255 } 256 257 func hcl2ValueFromFlatmapMap(m map[string]string, prefix string, ty cty.Type) (cty.Value, error) { 258 vals := make(map[string]cty.Value) 259 ety := ty.ElementType() 260 261 // if the container is unknown, there is no count string 262 listName := strings.TrimRight(prefix, ".") 263 if m[listName] == UnknownVariableValue { 264 return cty.UnknownVal(ty), nil 265 } 266 267 // We actually don't really care about the "count" of a map for our 268 // purposes here, but we do need to check if it _exists_ in order to 269 // recognize the difference between null (not set at all) and empty. 270 if strCount, exists := m[prefix+"%"]; !exists { 271 return cty.NullVal(ty), nil 272 } else if strCount == UnknownVariableValue { 273 return cty.UnknownVal(ty), nil 274 } 275 276 for fullKey := range m { 277 if !strings.HasPrefix(fullKey, prefix) { 278 continue 279 } 280 281 // The flatmap format doesn't allow us to distinguish between keys 282 // that contain periods and nested objects, so by convention a 283 // map is only ever of primitive type in flatmap, and we just assume 284 // that the remainder of the raw key (dots and all) is the key we 285 // want in the result value. 286 key := fullKey[len(prefix):] 287 if key == "%" { 288 // Ignore the "count" key 289 continue 290 } 291 292 val, err := hcl2ValueFromFlatmapValue(m, fullKey, ety) 293 if err != nil { 294 return cty.DynamicVal, err 295 } 296 vals[key] = val 297 } 298 299 if len(vals) == 0 { 300 return cty.MapValEmpty(ety), nil 301 } 302 return cty.MapVal(vals), nil 303 } 304 305 func hcl2ValueFromFlatmapList(m map[string]string, prefix string, ty cty.Type) (cty.Value, error) { 306 var vals []cty.Value 307 308 // if the container is unknown, there is no count string 309 listName := strings.TrimRight(prefix, ".") 310 if m[listName] == UnknownVariableValue { 311 return cty.UnknownVal(ty), nil 312 } 313 314 countStr, exists := m[prefix+"#"] 315 if !exists { 316 return cty.NullVal(ty), nil 317 } 318 if countStr == UnknownVariableValue { 319 return cty.UnknownVal(ty), nil 320 } 321 322 count, err := strconv.Atoi(countStr) 323 if err != nil { 324 return cty.DynamicVal, fmt.Errorf("invalid count value for %q in state: %w", prefix, err) 325 } 326 327 ety := ty.ElementType() 328 if count == 0 { 329 return cty.ListValEmpty(ety), nil 330 } 331 332 vals = make([]cty.Value, count) 333 for i := 0; i < count; i++ { 334 key := prefix + strconv.Itoa(i) 335 val, err := hcl2ValueFromFlatmapValue(m, key, ety) 336 if err != nil { 337 return cty.DynamicVal, err 338 } 339 vals[i] = val 340 } 341 342 return cty.ListVal(vals), nil 343 } 344 345 func hcl2ValueFromFlatmapSet(m map[string]string, prefix string, ty cty.Type) (cty.Value, error) { 346 var vals []cty.Value 347 ety := ty.ElementType() 348 349 // if the container is unknown, there is no count string 350 listName := strings.TrimRight(prefix, ".") 351 if m[listName] == UnknownVariableValue { 352 return cty.UnknownVal(ty), nil 353 } 354 355 strCount, exists := m[prefix+"#"] 356 if !exists { 357 return cty.NullVal(ty), nil 358 } else if strCount == UnknownVariableValue { 359 return cty.UnknownVal(ty), nil 360 } 361 362 // Keep track of keys we've seen, se we don't add the same set value 363 // multiple times. The cty.Set will normally de-duplicate values, but we may 364 // have unknown values that would not show as equivalent. 365 seen := map[string]bool{} 366 367 for fullKey := range m { 368 if !strings.HasPrefix(fullKey, prefix) { 369 continue 370 } 371 subKey := fullKey[len(prefix):] 372 if subKey == "#" { 373 // Ignore the "count" key 374 continue 375 } 376 key := fullKey 377 if dot := strings.IndexByte(subKey, '.'); dot != -1 { 378 key = fullKey[:dot+len(prefix)] 379 } 380 381 if seen[key] { 382 continue 383 } 384 385 seen[key] = true 386 387 // The flatmap format doesn't allow us to distinguish between keys 388 // that contain periods and nested objects, so by convention a 389 // map is only ever of primitive type in flatmap, and we just assume 390 // that the remainder of the raw key (dots and all) is the key we 391 // want in the result value. 392 393 val, err := hcl2ValueFromFlatmapValue(m, key, ety) 394 if err != nil { 395 return cty.DynamicVal, err 396 } 397 vals = append(vals, val) 398 } 399 400 if len(vals) == 0 && strCount == "1" { 401 // An empty set wouldn't be represented in the flatmap, so this must be 402 // a single empty object since the count is actually 1. 403 // Add an appropriately typed null value to the set. 404 var val cty.Value 405 switch { 406 case ety.IsMapType(): 407 val = cty.MapValEmpty(ety) 408 case ety.IsListType(): 409 val = cty.ListValEmpty(ety) 410 case ety.IsSetType(): 411 val = cty.SetValEmpty(ety) 412 case ety.IsObjectType(): 413 // TODO: cty.ObjectValEmpty 414 objectMap := map[string]cty.Value{} 415 for attr, ty := range ety.AttributeTypes() { 416 objectMap[attr] = cty.NullVal(ty) 417 } 418 val = cty.ObjectVal(objectMap) 419 default: 420 val = cty.NullVal(ety) 421 } 422 vals = append(vals, val) 423 424 } else if len(vals) == 0 { 425 return cty.SetValEmpty(ety), nil 426 } 427 428 return cty.SetVal(vals), nil 429 }