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 }