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