opentofu/internal/configs/hcl2shim/values_equiv.go
Martin Atkins 31349a9c3a Move configs/ to internal/configs/
This is part of a general effort to move all of Terraform's non-library
package surface under internal in order to reinforce that these are for
internal use within Terraform only.

If you were previously importing packages under this prefix into an
external codebase, you could pin to an earlier release tag as an interim
solution until you've make a plan to achieve the same functionality some
other way.
2021-05-17 14:09:07 -07:00

215 lines
6.7 KiB
Go

package hcl2shim
import (
"github.com/zclconf/go-cty/cty"
)
// ValuesSDKEquivalent returns true if both of the given values seem equivalent
// as far as the legacy SDK diffing code would be concerned.
//
// Since SDK diffing is a fuzzy, inexact operation, this function is also
// fuzzy and inexact. It will err on the side of returning false if it
// encounters an ambiguous situation. Ambiguity is most common in the presence
// of sets because in practice it is impossible to exactly correlate
// nonequal-but-equivalent set elements because they have no identity separate
// from their value.
//
// This must be used _only_ for comparing values for equivalence within the
// SDK planning code. It is only meaningful to compare the "prior state"
// provided by Terraform Core with the "planned new state" produced by the
// legacy SDK code via shims. In particular it is not valid to use this
// function with their the config value or the "proposed new state" value
// because they contain only the subset of data that Terraform Core itself is
// able to determine.
func ValuesSDKEquivalent(a, b cty.Value) bool {
if a == cty.NilVal || b == cty.NilVal {
// We don't generally expect nils to appear, but we'll allow them
// for robustness since the data structures produced by legacy SDK code
// can sometimes be non-ideal.
return a == b // equivalent if they are _both_ nil
}
if a.RawEquals(b) {
// Easy case. We use RawEquals because we want two unknowns to be
// considered equal here, whereas "Equals" would return unknown.
return true
}
if !a.IsKnown() || !b.IsKnown() {
// Two unknown values are equivalent regardless of type. A known is
// never equivalent to an unknown.
return a.IsKnown() == b.IsKnown()
}
if aZero, bZero := valuesSDKEquivalentIsNullOrZero(a), valuesSDKEquivalentIsNullOrZero(b); aZero || bZero {
// Two null/zero values are equivalent regardless of type. A non-zero is
// never equivalent to a zero.
return aZero == bZero
}
// If we get down here then we are guaranteed that both a and b are known,
// non-null values.
aTy := a.Type()
bTy := b.Type()
switch {
case aTy.IsSetType() && bTy.IsSetType():
return valuesSDKEquivalentSets(a, b)
case aTy.IsListType() && bTy.IsListType():
return valuesSDKEquivalentSequences(a, b)
case aTy.IsTupleType() && bTy.IsTupleType():
return valuesSDKEquivalentSequences(a, b)
case aTy.IsMapType() && bTy.IsMapType():
return valuesSDKEquivalentMappings(a, b)
case aTy.IsObjectType() && bTy.IsObjectType():
return valuesSDKEquivalentMappings(a, b)
case aTy == cty.Number && bTy == cty.Number:
return valuesSDKEquivalentNumbers(a, b)
default:
// We've now covered all the interesting cases, so anything that falls
// down here cannot be equivalent.
return false
}
}
// valuesSDKEquivalentIsNullOrZero returns true if the given value is either
// null or is the "zero value" (in the SDK/Go sense) for its type.
func valuesSDKEquivalentIsNullOrZero(v cty.Value) bool {
if v == cty.NilVal {
return true
}
ty := v.Type()
switch {
case !v.IsKnown():
return false
case v.IsNull():
return true
// After this point, v is always known and non-null
case ty.IsListType() || ty.IsSetType() || ty.IsMapType() || ty.IsObjectType() || ty.IsTupleType():
return v.LengthInt() == 0
case ty == cty.String:
return v.RawEquals(cty.StringVal(""))
case ty == cty.Number:
return v.RawEquals(cty.Zero)
case ty == cty.Bool:
return v.RawEquals(cty.False)
default:
// The above is exhaustive, but for robustness we'll consider anything
// else to _not_ be zero unless it is null.
return false
}
}
// valuesSDKEquivalentSets returns true only if each of the elements in a can
// be correlated with at least one equivalent element in b and vice-versa.
// This is a fuzzy operation that prefers to signal non-equivalence if it cannot
// be certain that all elements are accounted for.
func valuesSDKEquivalentSets(a, b cty.Value) bool {
if aLen, bLen := a.LengthInt(), b.LengthInt(); aLen != bLen {
return false
}
// Our methodology here is a little tricky, to deal with the fact that
// it's impossible to directly correlate two non-equal set elements because
// they don't have identities separate from their values.
// The approach is to count the number of equivalent elements each element
// of a has in b and vice-versa, and then return true only if each element
// in both sets has at least one equivalent.
as := a.AsValueSlice()
bs := b.AsValueSlice()
aeqs := make([]bool, len(as))
beqs := make([]bool, len(bs))
for ai, av := range as {
for bi, bv := range bs {
if ValuesSDKEquivalent(av, bv) {
aeqs[ai] = true
beqs[bi] = true
}
}
}
for _, eq := range aeqs {
if !eq {
return false
}
}
for _, eq := range beqs {
if !eq {
return false
}
}
return true
}
// valuesSDKEquivalentSequences decides equivalence for two sequence values
// (lists or tuples).
func valuesSDKEquivalentSequences(a, b cty.Value) bool {
as := a.AsValueSlice()
bs := b.AsValueSlice()
if len(as) != len(bs) {
return false
}
for i := range as {
if !ValuesSDKEquivalent(as[i], bs[i]) {
return false
}
}
return true
}
// valuesSDKEquivalentMappings decides equivalence for two mapping values
// (maps or objects).
func valuesSDKEquivalentMappings(a, b cty.Value) bool {
as := a.AsValueMap()
bs := b.AsValueMap()
if len(as) != len(bs) {
return false
}
for k, av := range as {
bv, ok := bs[k]
if !ok {
return false
}
if !ValuesSDKEquivalent(av, bv) {
return false
}
}
return true
}
// valuesSDKEquivalentNumbers decides equivalence for two number values based
// on the fact that the SDK uses int and float64 representations while
// cty (and thus Terraform Core) uses big.Float, and so we expect to lose
// precision in the round-trip.
//
// This does _not_ attempt to allow for an epsilon difference that may be
// caused by accumulated innacuracy in a float calculation, under the
// expectation that providers generally do not actually do compuations on
// floats and instead just pass string representations of them on verbatim
// to remote APIs. A remote API _itself_ may introduce inaccuracy, but that's
// a problem for the provider itself to deal with, based on its knowledge of
// the remote system, e.g. using DiffSuppressFunc.
func valuesSDKEquivalentNumbers(a, b cty.Value) bool {
if a.RawEquals(b) {
return true // easy
}
af := a.AsBigFloat()
bf := b.AsBigFloat()
if af.IsInt() != bf.IsInt() {
return false
}
if af.IsInt() && bf.IsInt() {
return false // a.RawEquals(b) test above is good enough for integers
}
// The SDK supports only int and float64, so if it's not an integer
// we know that only a float64-level of precision can possibly be
// significant.
af64, _ := af.Float64()
bf64, _ := bf.Float64()
return af64 == bf64
}