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  }