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

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package hcl2shim
     5  
     6  import (
     7  	"github.com/zclconf/go-cty/cty"
     8  )
     9  
    10  // ValuesSDKEquivalent returns true if both of the given values seem equivalent
    11  // as far as the legacy SDK diffing code would be concerned.
    12  //
    13  // Since SDK diffing is a fuzzy, inexact operation, this function is also
    14  // fuzzy and inexact. It will err on the side of returning false if it
    15  // encounters an ambiguous situation. Ambiguity is most common in the presence
    16  // of sets because in practice it is impossible to exactly correlate
    17  // nonequal-but-equivalent set elements because they have no identity separate
    18  // from their value.
    19  //
    20  // This must be used _only_ for comparing values for equivalence within the
    21  // SDK planning code. It is only meaningful to compare the "prior state"
    22  // provided by Terraform Core with the "planned new state" produced by the
    23  // legacy SDK code via shims. In particular it is not valid to use this
    24  // function with their the config value or the "proposed new state" value
    25  // because they contain only the subset of data that Terraform Core itself is
    26  // able to determine.
    27  func ValuesSDKEquivalent(a, b cty.Value) bool {
    28  	if a == cty.NilVal || b == cty.NilVal {
    29  		// We don't generally expect nils to appear, but we'll allow them
    30  		// for robustness since the data structures produced by legacy SDK code
    31  		// can sometimes be non-ideal.
    32  		return a == b // equivalent if they are _both_ nil
    33  	}
    34  	if a.RawEquals(b) {
    35  		// Easy case. We use RawEquals because we want two unknowns to be
    36  		// considered equal here, whereas "Equals" would return unknown.
    37  		return true
    38  	}
    39  	if !a.IsKnown() || !b.IsKnown() {
    40  		// Two unknown values are equivalent regardless of type. A known is
    41  		// never equivalent to an unknown.
    42  		return a.IsKnown() == b.IsKnown()
    43  	}
    44  	if aZero, bZero := valuesSDKEquivalentIsNullOrZero(a), valuesSDKEquivalentIsNullOrZero(b); aZero || bZero {
    45  		// Two null/zero values are equivalent regardless of type. A non-zero is
    46  		// never equivalent to a zero.
    47  		return aZero == bZero
    48  	}
    49  
    50  	// If we get down here then we are guaranteed that both a and b are known,
    51  	// non-null values.
    52  
    53  	aTy := a.Type()
    54  	bTy := b.Type()
    55  	switch {
    56  	case aTy.IsSetType() && bTy.IsSetType():
    57  		return valuesSDKEquivalentSets(a, b)
    58  	case aTy.IsListType() && bTy.IsListType():
    59  		return valuesSDKEquivalentSequences(a, b)
    60  	case aTy.IsTupleType() && bTy.IsTupleType():
    61  		return valuesSDKEquivalentSequences(a, b)
    62  	case aTy.IsMapType() && bTy.IsMapType():
    63  		return valuesSDKEquivalentMappings(a, b)
    64  	case aTy.IsObjectType() && bTy.IsObjectType():
    65  		return valuesSDKEquivalentMappings(a, b)
    66  	case aTy == cty.Number && bTy == cty.Number:
    67  		return valuesSDKEquivalentNumbers(a, b)
    68  	default:
    69  		// We've now covered all the interesting cases, so anything that falls
    70  		// down here cannot be equivalent.
    71  		return false
    72  	}
    73  }
    74  
    75  // valuesSDKEquivalentIsNullOrZero returns true if the given value is either
    76  // null or is the "zero value" (in the SDK/Go sense) for its type.
    77  func valuesSDKEquivalentIsNullOrZero(v cty.Value) bool {
    78  	if v == cty.NilVal {
    79  		return true
    80  	}
    81  
    82  	ty := v.Type()
    83  	switch {
    84  	case !v.IsKnown():
    85  		return false
    86  	case v.IsNull():
    87  		return true
    88  
    89  	// After this point, v is always known and non-null
    90  	case ty.IsListType() || ty.IsSetType() || ty.IsMapType() || ty.IsObjectType() || ty.IsTupleType():
    91  		return v.LengthInt() == 0
    92  	case ty == cty.String:
    93  		return v.RawEquals(cty.StringVal(""))
    94  	case ty == cty.Number:
    95  		return v.RawEquals(cty.Zero)
    96  	case ty == cty.Bool:
    97  		return v.RawEquals(cty.False)
    98  	default:
    99  		// The above is exhaustive, but for robustness we'll consider anything
   100  		// else to _not_ be zero unless it is null.
   101  		return false
   102  	}
   103  }
   104  
   105  // valuesSDKEquivalentSets returns true only if each of the elements in a can
   106  // be correlated with at least one equivalent element in b and vice-versa.
   107  // This is a fuzzy operation that prefers to signal non-equivalence if it cannot
   108  // be certain that all elements are accounted for.
   109  func valuesSDKEquivalentSets(a, b cty.Value) bool {
   110  	if aLen, bLen := a.LengthInt(), b.LengthInt(); aLen != bLen {
   111  		return false
   112  	}
   113  
   114  	// Our methodology here is a little tricky, to deal with the fact that
   115  	// it's impossible to directly correlate two non-equal set elements because
   116  	// they don't have identities separate from their values.
   117  	// The approach is to count the number of equivalent elements each element
   118  	// of a has in b and vice-versa, and then return true only if each element
   119  	// in both sets has at least one equivalent.
   120  	as := a.AsValueSlice()
   121  	bs := b.AsValueSlice()
   122  	aeqs := make([]bool, len(as))
   123  	beqs := make([]bool, len(bs))
   124  	for ai, av := range as {
   125  		for bi, bv := range bs {
   126  			if ValuesSDKEquivalent(av, bv) {
   127  				aeqs[ai] = true
   128  				beqs[bi] = true
   129  			}
   130  		}
   131  	}
   132  
   133  	for _, eq := range aeqs {
   134  		if !eq {
   135  			return false
   136  		}
   137  	}
   138  	for _, eq := range beqs {
   139  		if !eq {
   140  			return false
   141  		}
   142  	}
   143  	return true
   144  }
   145  
   146  // valuesSDKEquivalentSequences decides equivalence for two sequence values
   147  // (lists or tuples).
   148  func valuesSDKEquivalentSequences(a, b cty.Value) bool {
   149  	as := a.AsValueSlice()
   150  	bs := b.AsValueSlice()
   151  	if len(as) != len(bs) {
   152  		return false
   153  	}
   154  
   155  	for i := range as {
   156  		if !ValuesSDKEquivalent(as[i], bs[i]) {
   157  			return false
   158  		}
   159  	}
   160  	return true
   161  }
   162  
   163  // valuesSDKEquivalentMappings decides equivalence for two mapping values
   164  // (maps or objects).
   165  func valuesSDKEquivalentMappings(a, b cty.Value) bool {
   166  	as := a.AsValueMap()
   167  	bs := b.AsValueMap()
   168  	if len(as) != len(bs) {
   169  		return false
   170  	}
   171  
   172  	for k, av := range as {
   173  		bv, ok := bs[k]
   174  		if !ok {
   175  			return false
   176  		}
   177  		if !ValuesSDKEquivalent(av, bv) {
   178  			return false
   179  		}
   180  	}
   181  	return true
   182  }
   183  
   184  // valuesSDKEquivalentNumbers decides equivalence for two number values based
   185  // on the fact that the SDK uses int and float64 representations while
   186  // cty (and thus Terraform Core) uses big.Float, and so we expect to lose
   187  // precision in the round-trip.
   188  //
   189  // This does _not_ attempt to allow for an epsilon difference that may be
   190  // caused by accumulated innacuracy in a float calculation, under the
   191  // expectation that providers generally do not actually do compuations on
   192  // floats and instead just pass string representations of them on verbatim
   193  // to remote APIs. A remote API _itself_ may introduce inaccuracy, but that's
   194  // a problem for the provider itself to deal with, based on its knowledge of
   195  // the remote system, e.g. using DiffSuppressFunc.
   196  func valuesSDKEquivalentNumbers(a, b cty.Value) bool {
   197  	if a.RawEquals(b) {
   198  		return true // easy
   199  	}
   200  
   201  	af := a.AsBigFloat()
   202  	bf := b.AsBigFloat()
   203  
   204  	if af.IsInt() != bf.IsInt() {
   205  		return false
   206  	}
   207  	if af.IsInt() && bf.IsInt() {
   208  		return false // a.RawEquals(b) test above is good enough for integers
   209  	}
   210  
   211  	// The SDK supports only int and float64, so if it's not an integer
   212  	// we know that only a float64-level of precision can possibly be
   213  	// significant.
   214  	af64, _ := af.Float64()
   215  	bf64, _ := bf.Float64()
   216  	return af64 == bf64
   217  }