aboutsummaryrefslogtreecommitdiffhomepage
path: root/vendor/github.com/hashicorp/terraform/config/hcl2shim/values_equiv.go
blob: 92f0213d72483cd0757d3c01725a7514fbbb73d1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
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
}