]>
Commit | Line | Data |
---|---|---|
107c1cdb ND |
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 | } |