diff options
Diffstat (limited to 'vendor/github.com/hashicorp/terraform/plans/objchange/compatible.go')
-rw-r--r-- | vendor/github.com/hashicorp/terraform/plans/objchange/compatible.go | 437 |
1 files changed, 437 insertions, 0 deletions
diff --git a/vendor/github.com/hashicorp/terraform/plans/objchange/compatible.go b/vendor/github.com/hashicorp/terraform/plans/objchange/compatible.go new file mode 100644 index 0000000..8b7ef43 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/plans/objchange/compatible.go | |||
@@ -0,0 +1,437 @@ | |||
1 | package objchange | ||
2 | |||
3 | import ( | ||
4 | "fmt" | ||
5 | "strconv" | ||
6 | |||
7 | "github.com/zclconf/go-cty/cty" | ||
8 | "github.com/zclconf/go-cty/cty/convert" | ||
9 | |||
10 | "github.com/hashicorp/terraform/configs/configschema" | ||
11 | ) | ||
12 | |||
13 | // AssertObjectCompatible checks whether the given "actual" value is a valid | ||
14 | // completion of the possibly-partially-unknown "planned" value. | ||
15 | // | ||
16 | // This means that any known leaf value in "planned" must be equal to the | ||
17 | // corresponding value in "actual", and various other similar constraints. | ||
18 | // | ||
19 | // Any inconsistencies are reported by returning a non-zero number of errors. | ||
20 | // These errors are usually (but not necessarily) cty.PathError values | ||
21 | // referring to a particular nested value within the "actual" value. | ||
22 | // | ||
23 | // The two values must have types that conform to the given schema's implied | ||
24 | // type, or this function will panic. | ||
25 | func AssertObjectCompatible(schema *configschema.Block, planned, actual cty.Value) []error { | ||
26 | return assertObjectCompatible(schema, planned, actual, nil) | ||
27 | } | ||
28 | |||
29 | func assertObjectCompatible(schema *configschema.Block, planned, actual cty.Value, path cty.Path) []error { | ||
30 | var errs []error | ||
31 | if planned.IsNull() && !actual.IsNull() { | ||
32 | errs = append(errs, path.NewErrorf("was absent, but now present")) | ||
33 | return errs | ||
34 | } | ||
35 | if actual.IsNull() && !planned.IsNull() { | ||
36 | errs = append(errs, path.NewErrorf("was present, but now absent")) | ||
37 | return errs | ||
38 | } | ||
39 | if planned.IsNull() { | ||
40 | // No further checks possible if both values are null | ||
41 | return errs | ||
42 | } | ||
43 | |||
44 | for name, attrS := range schema.Attributes { | ||
45 | plannedV := planned.GetAttr(name) | ||
46 | actualV := actual.GetAttr(name) | ||
47 | |||
48 | path := append(path, cty.GetAttrStep{Name: name}) | ||
49 | moreErrs := assertValueCompatible(plannedV, actualV, path) | ||
50 | if attrS.Sensitive { | ||
51 | if len(moreErrs) > 0 { | ||
52 | // Use a vague placeholder message instead, to avoid disclosing | ||
53 | // sensitive information. | ||
54 | errs = append(errs, path.NewErrorf("inconsistent values for sensitive attribute")) | ||
55 | } | ||
56 | } else { | ||
57 | errs = append(errs, moreErrs...) | ||
58 | } | ||
59 | } | ||
60 | for name, blockS := range schema.BlockTypes { | ||
61 | plannedV := planned.GetAttr(name) | ||
62 | actualV := actual.GetAttr(name) | ||
63 | |||
64 | // As a special case, if there were any blocks whose leaf attributes | ||
65 | // are all unknown then we assume (possibly incorrectly) that the | ||
66 | // HCL dynamic block extension is in use with an unknown for_each | ||
67 | // argument, and so we will do looser validation here that allows | ||
68 | // for those blocks to have expanded into a different number of blocks | ||
69 | // if the for_each value is now known. | ||
70 | maybeUnknownBlocks := couldHaveUnknownBlockPlaceholder(plannedV, blockS, false) | ||
71 | |||
72 | path := append(path, cty.GetAttrStep{Name: name}) | ||
73 | switch blockS.Nesting { | ||
74 | case configschema.NestingSingle, configschema.NestingGroup: | ||
75 | // If an unknown block placeholder was present then the placeholder | ||
76 | // may have expanded out into zero blocks, which is okay. | ||
77 | if maybeUnknownBlocks && actualV.IsNull() { | ||
78 | continue | ||
79 | } | ||
80 | moreErrs := assertObjectCompatible(&blockS.Block, plannedV, actualV, path) | ||
81 | errs = append(errs, moreErrs...) | ||
82 | case configschema.NestingList: | ||
83 | // A NestingList might either be a list or a tuple, depending on | ||
84 | // whether there are dynamically-typed attributes inside. However, | ||
85 | // both support a similar-enough API that we can treat them the | ||
86 | // same for our purposes here. | ||
87 | if !plannedV.IsKnown() || plannedV.IsNull() || actualV.IsNull() { | ||
88 | continue | ||
89 | } | ||
90 | |||
91 | if maybeUnknownBlocks { | ||
92 | // When unknown blocks are present the final blocks may be | ||
93 | // at different indices than the planned blocks, so unfortunately | ||
94 | // we can't do our usual checks in this case without generating | ||
95 | // false negatives. | ||
96 | continue | ||
97 | } | ||
98 | |||
99 | plannedL := plannedV.LengthInt() | ||
100 | actualL := actualV.LengthInt() | ||
101 | if plannedL != actualL { | ||
102 | errs = append(errs, path.NewErrorf("block count changed from %d to %d", plannedL, actualL)) | ||
103 | continue | ||
104 | } | ||
105 | for it := plannedV.ElementIterator(); it.Next(); { | ||
106 | idx, plannedEV := it.Element() | ||
107 | if !actualV.HasIndex(idx).True() { | ||
108 | continue | ||
109 | } | ||
110 | actualEV := actualV.Index(idx) | ||
111 | moreErrs := assertObjectCompatible(&blockS.Block, plannedEV, actualEV, append(path, cty.IndexStep{Key: idx})) | ||
112 | errs = append(errs, moreErrs...) | ||
113 | } | ||
114 | case configschema.NestingMap: | ||
115 | // A NestingMap might either be a map or an object, depending on | ||
116 | // whether there are dynamically-typed attributes inside, but | ||
117 | // that's decided statically and so both values will have the same | ||
118 | // kind. | ||
119 | if plannedV.Type().IsObjectType() { | ||
120 | plannedAtys := plannedV.Type().AttributeTypes() | ||
121 | actualAtys := actualV.Type().AttributeTypes() | ||
122 | for k := range plannedAtys { | ||
123 | if _, ok := actualAtys[k]; !ok { | ||
124 | errs = append(errs, path.NewErrorf("block key %q has vanished", k)) | ||
125 | continue | ||
126 | } | ||
127 | |||
128 | plannedEV := plannedV.GetAttr(k) | ||
129 | actualEV := actualV.GetAttr(k) | ||
130 | moreErrs := assertObjectCompatible(&blockS.Block, plannedEV, actualEV, append(path, cty.GetAttrStep{Name: k})) | ||
131 | errs = append(errs, moreErrs...) | ||
132 | } | ||
133 | if !maybeUnknownBlocks { // new blocks may appear if unknown blocks were present in the plan | ||
134 | for k := range actualAtys { | ||
135 | if _, ok := plannedAtys[k]; !ok { | ||
136 | errs = append(errs, path.NewErrorf("new block key %q has appeared", k)) | ||
137 | continue | ||
138 | } | ||
139 | } | ||
140 | } | ||
141 | } else { | ||
142 | if !plannedV.IsKnown() || plannedV.IsNull() || actualV.IsNull() { | ||
143 | continue | ||
144 | } | ||
145 | plannedL := plannedV.LengthInt() | ||
146 | actualL := actualV.LengthInt() | ||
147 | if plannedL != actualL && !maybeUnknownBlocks { // new blocks may appear if unknown blocks were persent in the plan | ||
148 | errs = append(errs, path.NewErrorf("block count changed from %d to %d", plannedL, actualL)) | ||
149 | continue | ||
150 | } | ||
151 | for it := plannedV.ElementIterator(); it.Next(); { | ||
152 | idx, plannedEV := it.Element() | ||
153 | if !actualV.HasIndex(idx).True() { | ||
154 | continue | ||
155 | } | ||
156 | actualEV := actualV.Index(idx) | ||
157 | moreErrs := assertObjectCompatible(&blockS.Block, plannedEV, actualEV, append(path, cty.IndexStep{Key: idx})) | ||
158 | errs = append(errs, moreErrs...) | ||
159 | } | ||
160 | } | ||
161 | case configschema.NestingSet: | ||
162 | if !plannedV.IsKnown() || !actualV.IsKnown() || plannedV.IsNull() || actualV.IsNull() { | ||
163 | continue | ||
164 | } | ||
165 | |||
166 | setErrs := assertSetValuesCompatible(plannedV, actualV, path, func(plannedEV, actualEV cty.Value) bool { | ||
167 | errs := assertObjectCompatible(&blockS.Block, plannedEV, actualEV, append(path, cty.IndexStep{Key: actualEV})) | ||
168 | return len(errs) == 0 | ||
169 | }) | ||
170 | errs = append(errs, setErrs...) | ||
171 | |||
172 | // There can be fewer elements in a set after its elements are all | ||
173 | // known (values that turn out to be equal will coalesce) but the | ||
174 | // number of elements must never get larger. | ||
175 | plannedL := plannedV.LengthInt() | ||
176 | actualL := actualV.LengthInt() | ||
177 | if plannedL < actualL { | ||
178 | errs = append(errs, path.NewErrorf("block set length changed from %d to %d", plannedL, actualL)) | ||
179 | } | ||
180 | default: | ||
181 | panic(fmt.Sprintf("unsupported nesting mode %s", blockS.Nesting)) | ||
182 | } | ||
183 | } | ||
184 | return errs | ||
185 | } | ||
186 | |||
187 | func assertValueCompatible(planned, actual cty.Value, path cty.Path) []error { | ||
188 | // NOTE: We don't normally use the GoString rendering of cty.Value in | ||
189 | // user-facing error messages as a rule, but we make an exception | ||
190 | // for this function because we expect the user to pass this message on | ||
191 | // verbatim to the provider development team and so more detail is better. | ||
192 | |||
193 | var errs []error | ||
194 | if planned.Type() == cty.DynamicPseudoType { | ||
195 | // Anything goes, then | ||
196 | return errs | ||
197 | } | ||
198 | if problems := planned.Type().TestConformance(actual.Type()); len(problems) > 0 { | ||
199 | errs = append(errs, path.NewErrorf("wrong final value type: %s", convert.MismatchMessage(actual.Type(), planned.Type()))) | ||
200 | // If the types don't match then we can't do any other comparisons, | ||
201 | // so we bail early. | ||
202 | return errs | ||
203 | } | ||
204 | |||
205 | if !planned.IsKnown() { | ||
206 | // We didn't know what were going to end up with during plan, so | ||
207 | // anything goes during apply. | ||
208 | return errs | ||
209 | } | ||
210 | |||
211 | if actual.IsNull() { | ||
212 | if planned.IsNull() { | ||
213 | return nil | ||
214 | } | ||
215 | errs = append(errs, path.NewErrorf("was %#v, but now null", planned)) | ||
216 | return errs | ||
217 | } | ||
218 | if planned.IsNull() { | ||
219 | errs = append(errs, path.NewErrorf("was null, but now %#v", actual)) | ||
220 | return errs | ||
221 | } | ||
222 | |||
223 | ty := planned.Type() | ||
224 | switch { | ||
225 | |||
226 | case !actual.IsKnown(): | ||
227 | errs = append(errs, path.NewErrorf("was known, but now unknown")) | ||
228 | |||
229 | case ty.IsPrimitiveType(): | ||
230 | if !actual.Equals(planned).True() { | ||
231 | errs = append(errs, path.NewErrorf("was %#v, but now %#v", planned, actual)) | ||
232 | } | ||
233 | |||
234 | case ty.IsListType() || ty.IsMapType() || ty.IsTupleType(): | ||
235 | for it := planned.ElementIterator(); it.Next(); { | ||
236 | k, plannedV := it.Element() | ||
237 | if !actual.HasIndex(k).True() { | ||
238 | errs = append(errs, path.NewErrorf("element %s has vanished", indexStrForErrors(k))) | ||
239 | continue | ||
240 | } | ||
241 | |||
242 | actualV := actual.Index(k) | ||
243 | moreErrs := assertValueCompatible(plannedV, actualV, append(path, cty.IndexStep{Key: k})) | ||
244 | errs = append(errs, moreErrs...) | ||
245 | } | ||
246 | |||
247 | for it := actual.ElementIterator(); it.Next(); { | ||
248 | k, _ := it.Element() | ||
249 | if !planned.HasIndex(k).True() { | ||
250 | errs = append(errs, path.NewErrorf("new element %s has appeared", indexStrForErrors(k))) | ||
251 | } | ||
252 | } | ||
253 | |||
254 | case ty.IsObjectType(): | ||
255 | atys := ty.AttributeTypes() | ||
256 | for name := range atys { | ||
257 | // Because we already tested that the two values have the same type, | ||
258 | // we can assume that the same attributes are present in both and | ||
259 | // focus just on testing their values. | ||
260 | plannedV := planned.GetAttr(name) | ||
261 | actualV := actual.GetAttr(name) | ||
262 | moreErrs := assertValueCompatible(plannedV, actualV, append(path, cty.GetAttrStep{Name: name})) | ||
263 | errs = append(errs, moreErrs...) | ||
264 | } | ||
265 | |||
266 | case ty.IsSetType(): | ||
267 | // We can't really do anything useful for sets here because changing | ||
268 | // an unknown element to known changes the identity of the element, and | ||
269 | // so we can't correlate them properly. However, we will at least check | ||
270 | // to ensure that the number of elements is consistent, along with | ||
271 | // the general type-match checks we ran earlier in this function. | ||
272 | if planned.IsKnown() && !planned.IsNull() && !actual.IsNull() { | ||
273 | |||
274 | setErrs := assertSetValuesCompatible(planned, actual, path, func(plannedV, actualV cty.Value) bool { | ||
275 | errs := assertValueCompatible(plannedV, actualV, append(path, cty.IndexStep{Key: actualV})) | ||
276 | return len(errs) == 0 | ||
277 | }) | ||
278 | errs = append(errs, setErrs...) | ||
279 | |||
280 | // There can be fewer elements in a set after its elements are all | ||
281 | // known (values that turn out to be equal will coalesce) but the | ||
282 | // number of elements must never get larger. | ||
283 | |||
284 | plannedL := planned.LengthInt() | ||
285 | actualL := actual.LengthInt() | ||
286 | if plannedL < actualL { | ||
287 | errs = append(errs, path.NewErrorf("length changed from %d to %d", plannedL, actualL)) | ||
288 | } | ||
289 | } | ||
290 | } | ||
291 | |||
292 | return errs | ||
293 | } | ||
294 | |||
295 | func indexStrForErrors(v cty.Value) string { | ||
296 | switch v.Type() { | ||
297 | case cty.Number: | ||
298 | return v.AsBigFloat().Text('f', -1) | ||
299 | case cty.String: | ||
300 | return strconv.Quote(v.AsString()) | ||
301 | default: | ||
302 | // Should be impossible, since no other index types are allowed! | ||
303 | return fmt.Sprintf("%#v", v) | ||
304 | } | ||
305 | } | ||
306 | |||
307 | // couldHaveUnknownBlockPlaceholder is a heuristic that recognizes how the | ||
308 | // HCL dynamic block extension behaves when it's asked to expand a block whose | ||
309 | // for_each argument is unknown. In such cases, it generates a single placeholder | ||
310 | // block with all leaf attribute values unknown, and once the for_each | ||
311 | // expression becomes known the placeholder may be replaced with any number | ||
312 | // of blocks, so object compatibility checks would need to be more liberal. | ||
313 | // | ||
314 | // Set "nested" if testing a block that is nested inside a candidate block | ||
315 | // placeholder; this changes the interpretation of there being no blocks of | ||
316 | // a type to allow for there being zero nested blocks. | ||
317 | func couldHaveUnknownBlockPlaceholder(v cty.Value, blockS *configschema.NestedBlock, nested bool) bool { | ||
318 | switch blockS.Nesting { | ||
319 | case configschema.NestingSingle, configschema.NestingGroup: | ||
320 | if nested && v.IsNull() { | ||
321 | return true // for nested blocks, a single block being unset doesn't disqualify from being an unknown block placeholder | ||
322 | } | ||
323 | return couldBeUnknownBlockPlaceholderElement(v, &blockS.Block) | ||
324 | default: | ||
325 | // These situations should be impossible for correct providers, but | ||
326 | // we permit the legacy SDK to produce some incorrect outcomes | ||
327 | // for compatibility with its existing logic, and so we must be | ||
328 | // tolerant here. | ||
329 | if !v.IsKnown() { | ||
330 | return true | ||
331 | } | ||
332 | if v.IsNull() { | ||
333 | return false // treated as if the list were empty, so we would see zero iterations below | ||
334 | } | ||
335 | |||
336 | // For all other nesting modes, our value should be something iterable. | ||
337 | for it := v.ElementIterator(); it.Next(); { | ||
338 | _, ev := it.Element() | ||
339 | if couldBeUnknownBlockPlaceholderElement(ev, &blockS.Block) { | ||
340 | return true | ||
341 | } | ||
342 | } | ||
343 | |||
344 | // Our default changes depending on whether we're testing the candidate | ||
345 | // block itself or something nested inside of it: zero blocks of a type | ||
346 | // can never contain a dynamic block placeholder, but a dynamic block | ||
347 | // placeholder might contain zero blocks of one of its own nested block | ||
348 | // types, if none were set in the config at all. | ||
349 | return nested | ||
350 | } | ||
351 | } | ||
352 | |||
353 | func couldBeUnknownBlockPlaceholderElement(v cty.Value, schema *configschema.Block) bool { | ||
354 | if v.IsNull() { | ||
355 | return false // null value can never be a placeholder element | ||
356 | } | ||
357 | if !v.IsKnown() { | ||
358 | return true // this should never happen for well-behaved providers, but can happen with the legacy SDK opt-outs | ||
359 | } | ||
360 | for name := range schema.Attributes { | ||
361 | av := v.GetAttr(name) | ||
362 | |||
363 | // Unknown block placeholders contain only unknown or null attribute | ||
364 | // values, depending on whether or not a particular attribute was set | ||
365 | // explicitly inside the content block. Note that this is imprecise: | ||
366 | // non-placeholders can also match this, so this function can generate | ||
367 | // false positives. | ||
368 | if av.IsKnown() && !av.IsNull() { | ||
369 | return false | ||
370 | } | ||
371 | } | ||
372 | for name, blockS := range schema.BlockTypes { | ||
373 | if !couldHaveUnknownBlockPlaceholder(v.GetAttr(name), blockS, true) { | ||
374 | return false | ||
375 | } | ||
376 | } | ||
377 | return true | ||
378 | } | ||
379 | |||
380 | // assertSetValuesCompatible checks that each of the elements in a can | ||
381 | // be correlated with at least one equivalent element in b and vice-versa, | ||
382 | // using the given correlation function. | ||
383 | // | ||
384 | // This allows the number of elements in the sets to change as long as all | ||
385 | // elements in both sets can be correlated, making this function safe to use | ||
386 | // with sets that may contain unknown values as long as the unknown case is | ||
387 | // addressed in some reasonable way in the callback function. | ||
388 | // | ||
389 | // The callback always recieves values from set a as its first argument and | ||
390 | // values from set b in its second argument, so it is safe to use with | ||
391 | // non-commutative functions. | ||
392 | // | ||
393 | // As with assertValueCompatible, we assume that the target audience of error | ||
394 | // messages here is a provider developer (via a bug report from a user) and so | ||
395 | // we intentionally violate our usual rule of keeping cty implementation | ||
396 | // details out of error messages. | ||
397 | func assertSetValuesCompatible(planned, actual cty.Value, path cty.Path, f func(aVal, bVal cty.Value) bool) []error { | ||
398 | a := planned | ||
399 | b := actual | ||
400 | |||
401 | // Our methodology here is a little tricky, to deal with the fact that | ||
402 | // it's impossible to directly correlate two non-equal set elements because | ||
403 | // they don't have identities separate from their values. | ||
404 | // The approach is to count the number of equivalent elements each element | ||
405 | // of a has in b and vice-versa, and then return true only if each element | ||
406 | // in both sets has at least one equivalent. | ||
407 | as := a.AsValueSlice() | ||
408 | bs := b.AsValueSlice() | ||
409 | aeqs := make([]bool, len(as)) | ||
410 | beqs := make([]bool, len(bs)) | ||
411 | for ai, av := range as { | ||
412 | for bi, bv := range bs { | ||
413 | if f(av, bv) { | ||
414 | aeqs[ai] = true | ||
415 | beqs[bi] = true | ||
416 | } | ||
417 | } | ||
418 | } | ||
419 | |||
420 | var errs []error | ||
421 | for i, eq := range aeqs { | ||
422 | if !eq { | ||
423 | errs = append(errs, path.NewErrorf("planned set element %#v does not correlate with any element in actual", as[i])) | ||
424 | } | ||
425 | } | ||
426 | if len(errs) > 0 { | ||
427 | // Exit early since otherwise we're likely to generate duplicate | ||
428 | // error messages from the other perspective in the subsequent loop. | ||
429 | return errs | ||
430 | } | ||
431 | for i, eq := range beqs { | ||
432 | if !eq { | ||
433 | errs = append(errs, path.NewErrorf("actual set element %#v does not correlate with any element in plan", bs[i])) | ||
434 | } | ||
435 | } | ||
436 | return errs | ||
437 | } | ||