opentofu/internal/configs/hcl2shim/flatmap.go
namgyalangmo cb2e9119aa
Update copyright notice (#1232)
Signed-off-by: namgyalangmo <75657887+namgyalangmo@users.noreply.github.com>
2024-02-08 09:48:59 +00:00

430 lines
12 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package hcl2shim
import (
"fmt"
"strconv"
"strings"
"github.com/zclconf/go-cty/cty/convert"
"github.com/zclconf/go-cty/cty"
)
// FlatmapValueFromHCL2 converts a value from HCL2 (really, from the cty dynamic
// types library that HCL2 uses) to a map compatible with what would be
// produced by the "flatmap" package.
//
// The type of the given value informs the structure of the resulting map.
// The value must be of an object type or this function will panic.
//
// Flatmap values can only represent maps when they are of primitive types,
// so the given value must not have any maps of complex types or the result
// is undefined.
func FlatmapValueFromHCL2(v cty.Value) map[string]string {
if v.IsNull() {
return nil
}
if !v.Type().IsObjectType() {
panic(fmt.Sprintf("HCL2ValueFromFlatmap called on %#v", v.Type()))
}
m := make(map[string]string)
flatmapValueFromHCL2Map(m, "", v)
return m
}
func flatmapValueFromHCL2Value(m map[string]string, key string, val cty.Value) {
ty := val.Type()
switch {
case ty.IsPrimitiveType() || ty == cty.DynamicPseudoType:
flatmapValueFromHCL2Primitive(m, key, val)
case ty.IsObjectType() || ty.IsMapType():
flatmapValueFromHCL2Map(m, key+".", val)
case ty.IsTupleType() || ty.IsListType() || ty.IsSetType():
flatmapValueFromHCL2Seq(m, key+".", val)
default:
panic(fmt.Sprintf("cannot encode %s to flatmap", ty.FriendlyName()))
}
}
func flatmapValueFromHCL2Primitive(m map[string]string, key string, val cty.Value) {
if !val.IsKnown() {
m[key] = UnknownVariableValue
return
}
if val.IsNull() {
// Omit entirely
return
}
var err error
val, err = convert.Convert(val, cty.String)
if err != nil {
// Should not be possible, since all primitive types can convert to string.
panic(fmt.Sprintf("invalid primitive encoding to flatmap: %s", err))
}
m[key] = val.AsString()
}
func flatmapValueFromHCL2Map(m map[string]string, prefix string, val cty.Value) {
if val.IsNull() {
// Omit entirely
return
}
if !val.IsKnown() {
switch {
case val.Type().IsObjectType():
// Whole objects can't be unknown in flatmap, so instead we'll
// just write all of the attribute values out as unknown.
for name, aty := range val.Type().AttributeTypes() {
flatmapValueFromHCL2Value(m, prefix+name, cty.UnknownVal(aty))
}
default:
m[prefix+"%"] = UnknownVariableValue
}
return
}
len := 0
for it := val.ElementIterator(); it.Next(); {
ak, av := it.Element()
name := ak.AsString()
flatmapValueFromHCL2Value(m, prefix+name, av)
len++
}
if !val.Type().IsObjectType() { // objects don't have an explicit count included, since their attribute count is fixed
m[prefix+"%"] = strconv.Itoa(len)
}
}
func flatmapValueFromHCL2Seq(m map[string]string, prefix string, val cty.Value) {
if val.IsNull() {
// Omit entirely
return
}
if !val.IsKnown() {
m[prefix+"#"] = UnknownVariableValue
return
}
// For sets this won't actually generate exactly what helper/schema would've
// generated, because we don't have access to the set key function it
// would've used. However, in practice it doesn't actually matter what the
// keys are as long as they are unique, so we'll just generate sequential
// indexes for them as if it were a list.
//
// An important implication of this, however, is that the set ordering will
// not be consistent across mutations and so different keys may be assigned
// to the same value when round-tripping. Since this shim is intended to
// be short-lived and not used for round-tripping, we accept this.
i := 0
for it := val.ElementIterator(); it.Next(); {
_, av := it.Element()
key := prefix + strconv.Itoa(i)
flatmapValueFromHCL2Value(m, key, av)
i++
}
m[prefix+"#"] = strconv.Itoa(i)
}
// HCL2ValueFromFlatmap converts a map compatible with what would be produced
// by the "flatmap" package to a HCL2 (really, the cty dynamic types library
// that HCL2 uses) object type.
//
// The intended result type must be provided in order to guide how the
// map contents are decoded. This must be an object type or this function
// will panic.
//
// Flatmap values can only represent maps when they are of primitive types,
// so the given type must not have any maps of complex types or the result
// is undefined.
//
// The result may contain null values if the given map does not contain keys
// for all of the different key paths implied by the given type.
func HCL2ValueFromFlatmap(m map[string]string, ty cty.Type) (cty.Value, error) {
if m == nil {
return cty.NullVal(ty), nil
}
if !ty.IsObjectType() {
panic(fmt.Sprintf("HCL2ValueFromFlatmap called on %#v", ty))
}
return hcl2ValueFromFlatmapObject(m, "", ty.AttributeTypes())
}
func hcl2ValueFromFlatmapValue(m map[string]string, key string, ty cty.Type) (cty.Value, error) {
var val cty.Value
var err error
switch {
case ty.IsPrimitiveType():
val, err = hcl2ValueFromFlatmapPrimitive(m, key, ty)
case ty.IsObjectType():
val, err = hcl2ValueFromFlatmapObject(m, key+".", ty.AttributeTypes())
case ty.IsTupleType():
val, err = hcl2ValueFromFlatmapTuple(m, key+".", ty.TupleElementTypes())
case ty.IsMapType():
val, err = hcl2ValueFromFlatmapMap(m, key+".", ty)
case ty.IsListType():
val, err = hcl2ValueFromFlatmapList(m, key+".", ty)
case ty.IsSetType():
val, err = hcl2ValueFromFlatmapSet(m, key+".", ty)
default:
err = fmt.Errorf("cannot decode %s from flatmap", ty.FriendlyName())
}
if err != nil {
return cty.DynamicVal, err
}
return val, nil
}
func hcl2ValueFromFlatmapPrimitive(m map[string]string, key string, ty cty.Type) (cty.Value, error) {
rawVal, exists := m[key]
if !exists {
return cty.NullVal(ty), nil
}
if rawVal == UnknownVariableValue {
return cty.UnknownVal(ty), nil
}
var err error
val := cty.StringVal(rawVal)
val, err = convert.Convert(val, ty)
if err != nil {
// This should never happen for _valid_ input, but flatmap data might
// be tampered with by the user and become invalid.
return cty.DynamicVal, fmt.Errorf("invalid value for %q in state: %w", key, err)
}
return val, nil
}
func hcl2ValueFromFlatmapObject(m map[string]string, prefix string, atys map[string]cty.Type) (cty.Value, error) {
vals := make(map[string]cty.Value)
for name, aty := range atys {
val, err := hcl2ValueFromFlatmapValue(m, prefix+name, aty)
if err != nil {
return cty.DynamicVal, err
}
vals[name] = val
}
return cty.ObjectVal(vals), nil
}
func hcl2ValueFromFlatmapTuple(m map[string]string, prefix string, etys []cty.Type) (cty.Value, error) {
var vals []cty.Value
// if the container is unknown, there is no count string
listName := strings.TrimRight(prefix, ".")
if m[listName] == UnknownVariableValue {
return cty.UnknownVal(cty.Tuple(etys)), nil
}
countStr, exists := m[prefix+"#"]
if !exists {
return cty.NullVal(cty.Tuple(etys)), nil
}
if countStr == UnknownVariableValue {
return cty.UnknownVal(cty.Tuple(etys)), nil
}
count, err := strconv.Atoi(countStr)
if err != nil {
return cty.DynamicVal, fmt.Errorf("invalid count value for %q in state: %w", prefix, err)
}
if count != len(etys) {
return cty.DynamicVal, fmt.Errorf("wrong number of values for %q in state: got %d, but need %d", prefix, count, len(etys))
}
vals = make([]cty.Value, len(etys))
for i, ety := range etys {
key := prefix + strconv.Itoa(i)
val, err := hcl2ValueFromFlatmapValue(m, key, ety)
if err != nil {
return cty.DynamicVal, err
}
vals[i] = val
}
return cty.TupleVal(vals), nil
}
func hcl2ValueFromFlatmapMap(m map[string]string, prefix string, ty cty.Type) (cty.Value, error) {
vals := make(map[string]cty.Value)
ety := ty.ElementType()
// if the container is unknown, there is no count string
listName := strings.TrimRight(prefix, ".")
if m[listName] == UnknownVariableValue {
return cty.UnknownVal(ty), nil
}
// We actually don't really care about the "count" of a map for our
// purposes here, but we do need to check if it _exists_ in order to
// recognize the difference between null (not set at all) and empty.
if strCount, exists := m[prefix+"%"]; !exists {
return cty.NullVal(ty), nil
} else if strCount == UnknownVariableValue {
return cty.UnknownVal(ty), nil
}
for fullKey := range m {
if !strings.HasPrefix(fullKey, prefix) {
continue
}
// The flatmap format doesn't allow us to distinguish between keys
// that contain periods and nested objects, so by convention a
// map is only ever of primitive type in flatmap, and we just assume
// that the remainder of the raw key (dots and all) is the key we
// want in the result value.
key := fullKey[len(prefix):]
if key == "%" {
// Ignore the "count" key
continue
}
val, err := hcl2ValueFromFlatmapValue(m, fullKey, ety)
if err != nil {
return cty.DynamicVal, err
}
vals[key] = val
}
if len(vals) == 0 {
return cty.MapValEmpty(ety), nil
}
return cty.MapVal(vals), nil
}
func hcl2ValueFromFlatmapList(m map[string]string, prefix string, ty cty.Type) (cty.Value, error) {
var vals []cty.Value
// if the container is unknown, there is no count string
listName := strings.TrimRight(prefix, ".")
if m[listName] == UnknownVariableValue {
return cty.UnknownVal(ty), nil
}
countStr, exists := m[prefix+"#"]
if !exists {
return cty.NullVal(ty), nil
}
if countStr == UnknownVariableValue {
return cty.UnknownVal(ty), nil
}
count, err := strconv.Atoi(countStr)
if err != nil {
return cty.DynamicVal, fmt.Errorf("invalid count value for %q in state: %w", prefix, err)
}
ety := ty.ElementType()
if count == 0 {
return cty.ListValEmpty(ety), nil
}
vals = make([]cty.Value, count)
for i := 0; i < count; i++ {
key := prefix + strconv.Itoa(i)
val, err := hcl2ValueFromFlatmapValue(m, key, ety)
if err != nil {
return cty.DynamicVal, err
}
vals[i] = val
}
return cty.ListVal(vals), nil
}
func hcl2ValueFromFlatmapSet(m map[string]string, prefix string, ty cty.Type) (cty.Value, error) {
var vals []cty.Value
ety := ty.ElementType()
// if the container is unknown, there is no count string
listName := strings.TrimRight(prefix, ".")
if m[listName] == UnknownVariableValue {
return cty.UnknownVal(ty), nil
}
strCount, exists := m[prefix+"#"]
if !exists {
return cty.NullVal(ty), nil
} else if strCount == UnknownVariableValue {
return cty.UnknownVal(ty), nil
}
// Keep track of keys we've seen, se we don't add the same set value
// multiple times. The cty.Set will normally de-duplicate values, but we may
// have unknown values that would not show as equivalent.
seen := map[string]bool{}
for fullKey := range m {
if !strings.HasPrefix(fullKey, prefix) {
continue
}
subKey := fullKey[len(prefix):]
if subKey == "#" {
// Ignore the "count" key
continue
}
key := fullKey
if dot := strings.IndexByte(subKey, '.'); dot != -1 {
key = fullKey[:dot+len(prefix)]
}
if seen[key] {
continue
}
seen[key] = true
// The flatmap format doesn't allow us to distinguish between keys
// that contain periods and nested objects, so by convention a
// map is only ever of primitive type in flatmap, and we just assume
// that the remainder of the raw key (dots and all) is the key we
// want in the result value.
val, err := hcl2ValueFromFlatmapValue(m, key, ety)
if err != nil {
return cty.DynamicVal, err
}
vals = append(vals, val)
}
if len(vals) == 0 && strCount == "1" {
// An empty set wouldn't be represented in the flatmap, so this must be
// a single empty object since the count is actually 1.
// Add an appropriately typed null value to the set.
var val cty.Value
switch {
case ety.IsMapType():
val = cty.MapValEmpty(ety)
case ety.IsListType():
val = cty.ListValEmpty(ety)
case ety.IsSetType():
val = cty.SetValEmpty(ety)
case ety.IsObjectType():
// TODO: cty.ObjectValEmpty
objectMap := map[string]cty.Value{}
for attr, ty := range ety.AttributeTypes() {
objectMap[attr] = cty.NullVal(ty)
}
val = cty.ObjectVal(objectMap)
default:
val = cty.NullVal(ety)
}
vals = append(vals, val)
} else if len(vals) == 0 {
return cty.SetValEmpty(ety), nil
}
return cty.SetVal(vals), nil
}