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 }