github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/configs/hcl2shim/values.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package hcl2shim
     5  
     6  import (
     7  	"fmt"
     8  	"math/big"
     9  
    10  	"github.com/zclconf/go-cty/cty"
    11  
    12  	"github.com/terramate-io/tf/configs/configschema"
    13  )
    14  
    15  // UnknownVariableValue is a sentinel value that can be used
    16  // to denote that the value of a variable is unknown at this time.
    17  // RawConfig uses this information to build up data about
    18  // unknown keys.
    19  const UnknownVariableValue = "74D93920-ED26-11E3-AC10-0800200C9A66"
    20  
    21  // ConfigValueFromHCL2Block is like ConfigValueFromHCL2 but it works only for
    22  // known object values and uses the provided block schema to perform some
    23  // additional normalization to better mimic the shape of value that the old
    24  // HCL1/HIL-based codepaths would've produced.
    25  //
    26  // In particular, it discards the collections that we use to represent nested
    27  // blocks (other than NestingSingle) if they are empty, which better mimics
    28  // the HCL1 behavior because HCL1 had no knowledge of the schema and so didn't
    29  // know that an unspecified block _could_ exist.
    30  //
    31  // The given object value must conform to the schema's implied type or this
    32  // function will panic or produce incorrect results.
    33  //
    34  // This is primarily useful for the final transition from new-style values to
    35  // terraform.ResourceConfig before calling to a legacy provider, since
    36  // helper/schema (the old provider SDK) is particularly sensitive to these
    37  // subtle differences within its validation code.
    38  func ConfigValueFromHCL2Block(v cty.Value, schema *configschema.Block) map[string]interface{} {
    39  	if v.IsNull() {
    40  		return nil
    41  	}
    42  	if !v.IsKnown() {
    43  		panic("ConfigValueFromHCL2Block used with unknown value")
    44  	}
    45  	if !v.Type().IsObjectType() {
    46  		panic(fmt.Sprintf("ConfigValueFromHCL2Block used with non-object value %#v", v))
    47  	}
    48  
    49  	atys := v.Type().AttributeTypes()
    50  	ret := make(map[string]interface{})
    51  
    52  	for name := range schema.Attributes {
    53  		if _, exists := atys[name]; !exists {
    54  			continue
    55  		}
    56  
    57  		av := v.GetAttr(name)
    58  		if av.IsNull() {
    59  			// Skip nulls altogether, to better mimic how HCL1 would behave
    60  			continue
    61  		}
    62  		ret[name] = ConfigValueFromHCL2(av)
    63  	}
    64  
    65  	for name, blockS := range schema.BlockTypes {
    66  		if _, exists := atys[name]; !exists {
    67  			continue
    68  		}
    69  		bv := v.GetAttr(name)
    70  		if !bv.IsKnown() {
    71  			ret[name] = UnknownVariableValue
    72  			continue
    73  		}
    74  		if bv.IsNull() {
    75  			continue
    76  		}
    77  
    78  		switch blockS.Nesting {
    79  
    80  		case configschema.NestingSingle, configschema.NestingGroup:
    81  			ret[name] = ConfigValueFromHCL2Block(bv, &blockS.Block)
    82  
    83  		case configschema.NestingList, configschema.NestingSet:
    84  			l := bv.LengthInt()
    85  			if l == 0 {
    86  				// skip empty collections to better mimic how HCL1 would behave
    87  				continue
    88  			}
    89  
    90  			elems := make([]interface{}, 0, l)
    91  			for it := bv.ElementIterator(); it.Next(); {
    92  				_, ev := it.Element()
    93  				if !ev.IsKnown() {
    94  					elems = append(elems, UnknownVariableValue)
    95  					continue
    96  				}
    97  				elems = append(elems, ConfigValueFromHCL2Block(ev, &blockS.Block))
    98  			}
    99  			ret[name] = elems
   100  
   101  		case configschema.NestingMap:
   102  			if bv.LengthInt() == 0 {
   103  				// skip empty collections to better mimic how HCL1 would behave
   104  				continue
   105  			}
   106  
   107  			elems := make(map[string]interface{})
   108  			for it := bv.ElementIterator(); it.Next(); {
   109  				ek, ev := it.Element()
   110  				if !ev.IsKnown() {
   111  					elems[ek.AsString()] = UnknownVariableValue
   112  					continue
   113  				}
   114  				elems[ek.AsString()] = ConfigValueFromHCL2Block(ev, &blockS.Block)
   115  			}
   116  			ret[name] = elems
   117  		}
   118  	}
   119  
   120  	return ret
   121  }
   122  
   123  // ConfigValueFromHCL2 converts a value from HCL2 (really, from the cty dynamic
   124  // types library that HCL2 uses) to a value type that matches what would've
   125  // been produced from the HCL-based interpolator for an equivalent structure.
   126  //
   127  // This function will transform a cty null value into a Go nil value, which
   128  // isn't a possible outcome of the HCL/HIL-based decoder and so callers may
   129  // need to detect and reject any null values.
   130  func ConfigValueFromHCL2(v cty.Value) interface{} {
   131  	if !v.IsKnown() {
   132  		return UnknownVariableValue
   133  	}
   134  	if v.IsNull() {
   135  		return nil
   136  	}
   137  
   138  	switch v.Type() {
   139  	case cty.Bool:
   140  		return v.True() // like HCL.BOOL
   141  	case cty.String:
   142  		return v.AsString() // like HCL token.STRING or token.HEREDOC
   143  	case cty.Number:
   144  		// We can't match HCL _exactly_ here because it distinguishes between
   145  		// int and float values, but we'll get as close as we can by using
   146  		// an int if the number is exactly representable, and a float if not.
   147  		// The conversion to float will force precision to that of a float64,
   148  		// which is potentially losing information from the specific number
   149  		// given, but no worse than what HCL would've done in its own conversion
   150  		// to float.
   151  
   152  		f := v.AsBigFloat()
   153  		if i, acc := f.Int64(); acc == big.Exact {
   154  			// if we're on a 32-bit system and the number is too big for 32-bit
   155  			// int then we'll fall through here and use a float64.
   156  			const MaxInt = int(^uint(0) >> 1)
   157  			const MinInt = -MaxInt - 1
   158  			if i <= int64(MaxInt) && i >= int64(MinInt) {
   159  				return int(i) // Like HCL token.NUMBER
   160  			}
   161  		}
   162  
   163  		f64, _ := f.Float64()
   164  		return f64 // like HCL token.FLOAT
   165  	}
   166  
   167  	if v.Type().IsListType() || v.Type().IsSetType() || v.Type().IsTupleType() {
   168  		l := make([]interface{}, 0, v.LengthInt())
   169  		it := v.ElementIterator()
   170  		for it.Next() {
   171  			_, ev := it.Element()
   172  			l = append(l, ConfigValueFromHCL2(ev))
   173  		}
   174  		return l
   175  	}
   176  
   177  	if v.Type().IsMapType() || v.Type().IsObjectType() {
   178  		l := make(map[string]interface{})
   179  		it := v.ElementIterator()
   180  		for it.Next() {
   181  			ek, ev := it.Element()
   182  			cv := ConfigValueFromHCL2(ev)
   183  			if cv != nil {
   184  				l[ek.AsString()] = cv
   185  			}
   186  		}
   187  		return l
   188  	}
   189  
   190  	// If we fall out here then we have some weird type that we haven't
   191  	// accounted for. This should never happen unless the caller is using
   192  	// capsule types, and we don't currently have any such types defined.
   193  	panic(fmt.Errorf("can't convert %#v to config value", v))
   194  }
   195  
   196  // HCL2ValueFromConfigValue is the opposite of configValueFromHCL2: it takes
   197  // a value as would be returned from the old interpolator and turns it into
   198  // a cty.Value so it can be used within, for example, an HCL2 EvalContext.
   199  func HCL2ValueFromConfigValue(v interface{}) cty.Value {
   200  	if v == nil {
   201  		return cty.NullVal(cty.DynamicPseudoType)
   202  	}
   203  	if v == UnknownVariableValue {
   204  		return cty.DynamicVal
   205  	}
   206  
   207  	switch tv := v.(type) {
   208  	case bool:
   209  		return cty.BoolVal(tv)
   210  	case string:
   211  		return cty.StringVal(tv)
   212  	case int:
   213  		return cty.NumberIntVal(int64(tv))
   214  	case float64:
   215  		return cty.NumberFloatVal(tv)
   216  	case []interface{}:
   217  		vals := make([]cty.Value, len(tv))
   218  		for i, ev := range tv {
   219  			vals[i] = HCL2ValueFromConfigValue(ev)
   220  		}
   221  		return cty.TupleVal(vals)
   222  	case map[string]interface{}:
   223  		vals := map[string]cty.Value{}
   224  		for k, ev := range tv {
   225  			vals[k] = HCL2ValueFromConfigValue(ev)
   226  		}
   227  		return cty.ObjectVal(vals)
   228  	default:
   229  		// HCL/HIL should never generate anything that isn't caught by
   230  		// the above, so if we get here something has gone very wrong.
   231  		panic(fmt.Errorf("can't convert %#v to cty.Value", v))
   232  	}
   233  }