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  }