215 lines
6.7 KiB
Go
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
|
|
}
|