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