| 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 |
| } |