]> git.immae.eu Git - github/fretlink/terraform-provider-statuscake.git/blob - vendor/github.com/hashicorp/terraform/plans/objchange/compatible.go
d85086c97c184282c5b7ba79fdbcc480b853107f
[github/fretlink/terraform-provider-statuscake.git] / vendor / github.com / hashicorp / terraform / plans / objchange / compatible.go
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() || !actualV.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 if maybeUnknownBlocks {
173 // When unknown blocks are present the final number of blocks
174 // may be different, either because the unknown set values
175 // become equal and are collapsed, or the count is unknown due
176 // a dynamic block. Unfortunately this means we can't do our
177 // usual checks in this case without generating false
178 // negatives.
179 continue
180 }
181
182 // There can be fewer elements in a set after its elements are all
183 // known (values that turn out to be equal will coalesce) but the
184 // number of elements must never get larger.
185 plannedL := plannedV.LengthInt()
186 actualL := actualV.LengthInt()
187 if plannedL < actualL {
188 errs = append(errs, path.NewErrorf("block set length changed from %d to %d", plannedL, actualL))
189 }
190 default:
191 panic(fmt.Sprintf("unsupported nesting mode %s", blockS.Nesting))
192 }
193 }
194 return errs
195 }
196
197 func assertValueCompatible(planned, actual cty.Value, path cty.Path) []error {
198 // NOTE: We don't normally use the GoString rendering of cty.Value in
199 // user-facing error messages as a rule, but we make an exception
200 // for this function because we expect the user to pass this message on
201 // verbatim to the provider development team and so more detail is better.
202
203 var errs []error
204 if planned.Type() == cty.DynamicPseudoType {
205 // Anything goes, then
206 return errs
207 }
208 if problems := planned.Type().TestConformance(actual.Type()); len(problems) > 0 {
209 errs = append(errs, path.NewErrorf("wrong final value type: %s", convert.MismatchMessage(actual.Type(), planned.Type())))
210 // If the types don't match then we can't do any other comparisons,
211 // so we bail early.
212 return errs
213 }
214
215 if !planned.IsKnown() {
216 // We didn't know what were going to end up with during plan, so
217 // anything goes during apply.
218 return errs
219 }
220
221 if actual.IsNull() {
222 if planned.IsNull() {
223 return nil
224 }
225 errs = append(errs, path.NewErrorf("was %#v, but now null", planned))
226 return errs
227 }
228 if planned.IsNull() {
229 errs = append(errs, path.NewErrorf("was null, but now %#v", actual))
230 return errs
231 }
232
233 ty := planned.Type()
234 switch {
235
236 case !actual.IsKnown():
237 errs = append(errs, path.NewErrorf("was known, but now unknown"))
238
239 case ty.IsPrimitiveType():
240 if !actual.Equals(planned).True() {
241 errs = append(errs, path.NewErrorf("was %#v, but now %#v", planned, actual))
242 }
243
244 case ty.IsListType() || ty.IsMapType() || ty.IsTupleType():
245 for it := planned.ElementIterator(); it.Next(); {
246 k, plannedV := it.Element()
247 if !actual.HasIndex(k).True() {
248 errs = append(errs, path.NewErrorf("element %s has vanished", indexStrForErrors(k)))
249 continue
250 }
251
252 actualV := actual.Index(k)
253 moreErrs := assertValueCompatible(plannedV, actualV, append(path, cty.IndexStep{Key: k}))
254 errs = append(errs, moreErrs...)
255 }
256
257 for it := actual.ElementIterator(); it.Next(); {
258 k, _ := it.Element()
259 if !planned.HasIndex(k).True() {
260 errs = append(errs, path.NewErrorf("new element %s has appeared", indexStrForErrors(k)))
261 }
262 }
263
264 case ty.IsObjectType():
265 atys := ty.AttributeTypes()
266 for name := range atys {
267 // Because we already tested that the two values have the same type,
268 // we can assume that the same attributes are present in both and
269 // focus just on testing their values.
270 plannedV := planned.GetAttr(name)
271 actualV := actual.GetAttr(name)
272 moreErrs := assertValueCompatible(plannedV, actualV, append(path, cty.GetAttrStep{Name: name}))
273 errs = append(errs, moreErrs...)
274 }
275
276 case ty.IsSetType():
277 // We can't really do anything useful for sets here because changing
278 // an unknown element to known changes the identity of the element, and
279 // so we can't correlate them properly. However, we will at least check
280 // to ensure that the number of elements is consistent, along with
281 // the general type-match checks we ran earlier in this function.
282 if planned.IsKnown() && !planned.IsNull() && !actual.IsNull() {
283
284 setErrs := assertSetValuesCompatible(planned, actual, path, func(plannedV, actualV cty.Value) bool {
285 errs := assertValueCompatible(plannedV, actualV, append(path, cty.IndexStep{Key: actualV}))
286 return len(errs) == 0
287 })
288 errs = append(errs, setErrs...)
289
290 // There can be fewer elements in a set after its elements are all
291 // known (values that turn out to be equal will coalesce) but the
292 // number of elements must never get larger.
293
294 plannedL := planned.LengthInt()
295 actualL := actual.LengthInt()
296 if plannedL < actualL {
297 errs = append(errs, path.NewErrorf("length changed from %d to %d", plannedL, actualL))
298 }
299 }
300 }
301
302 return errs
303 }
304
305 func indexStrForErrors(v cty.Value) string {
306 switch v.Type() {
307 case cty.Number:
308 return v.AsBigFloat().Text('f', -1)
309 case cty.String:
310 return strconv.Quote(v.AsString())
311 default:
312 // Should be impossible, since no other index types are allowed!
313 return fmt.Sprintf("%#v", v)
314 }
315 }
316
317 // couldHaveUnknownBlockPlaceholder is a heuristic that recognizes how the
318 // HCL dynamic block extension behaves when it's asked to expand a block whose
319 // for_each argument is unknown. In such cases, it generates a single placeholder
320 // block with all leaf attribute values unknown, and once the for_each
321 // expression becomes known the placeholder may be replaced with any number
322 // of blocks, so object compatibility checks would need to be more liberal.
323 //
324 // Set "nested" if testing a block that is nested inside a candidate block
325 // placeholder; this changes the interpretation of there being no blocks of
326 // a type to allow for there being zero nested blocks.
327 func couldHaveUnknownBlockPlaceholder(v cty.Value, blockS *configschema.NestedBlock, nested bool) bool {
328 switch blockS.Nesting {
329 case configschema.NestingSingle, configschema.NestingGroup:
330 if nested && v.IsNull() {
331 return true // for nested blocks, a single block being unset doesn't disqualify from being an unknown block placeholder
332 }
333 return couldBeUnknownBlockPlaceholderElement(v, &blockS.Block)
334 default:
335 // These situations should be impossible for correct providers, but
336 // we permit the legacy SDK to produce some incorrect outcomes
337 // for compatibility with its existing logic, and so we must be
338 // tolerant here.
339 if !v.IsKnown() {
340 return true
341 }
342 if v.IsNull() {
343 return false // treated as if the list were empty, so we would see zero iterations below
344 }
345
346 // For all other nesting modes, our value should be something iterable.
347 for it := v.ElementIterator(); it.Next(); {
348 _, ev := it.Element()
349 if couldBeUnknownBlockPlaceholderElement(ev, &blockS.Block) {
350 return true
351 }
352 }
353
354 // Our default changes depending on whether we're testing the candidate
355 // block itself or something nested inside of it: zero blocks of a type
356 // can never contain a dynamic block placeholder, but a dynamic block
357 // placeholder might contain zero blocks of one of its own nested block
358 // types, if none were set in the config at all.
359 return nested
360 }
361 }
362
363 func couldBeUnknownBlockPlaceholderElement(v cty.Value, schema *configschema.Block) bool {
364 if v.IsNull() {
365 return false // null value can never be a placeholder element
366 }
367 if !v.IsKnown() {
368 return true // this should never happen for well-behaved providers, but can happen with the legacy SDK opt-outs
369 }
370 for name := range schema.Attributes {
371 av := v.GetAttr(name)
372
373 // Unknown block placeholders contain only unknown or null attribute
374 // values, depending on whether or not a particular attribute was set
375 // explicitly inside the content block. Note that this is imprecise:
376 // non-placeholders can also match this, so this function can generate
377 // false positives.
378 if av.IsKnown() && !av.IsNull() {
379 return false
380 }
381 }
382 for name, blockS := range schema.BlockTypes {
383 if !couldHaveUnknownBlockPlaceholder(v.GetAttr(name), blockS, true) {
384 return false
385 }
386 }
387 return true
388 }
389
390 // assertSetValuesCompatible checks that each of the elements in a can
391 // be correlated with at least one equivalent element in b and vice-versa,
392 // using the given correlation function.
393 //
394 // This allows the number of elements in the sets to change as long as all
395 // elements in both sets can be correlated, making this function safe to use
396 // with sets that may contain unknown values as long as the unknown case is
397 // addressed in some reasonable way in the callback function.
398 //
399 // The callback always recieves values from set a as its first argument and
400 // values from set b in its second argument, so it is safe to use with
401 // non-commutative functions.
402 //
403 // As with assertValueCompatible, we assume that the target audience of error
404 // messages here is a provider developer (via a bug report from a user) and so
405 // we intentionally violate our usual rule of keeping cty implementation
406 // details out of error messages.
407 func assertSetValuesCompatible(planned, actual cty.Value, path cty.Path, f func(aVal, bVal cty.Value) bool) []error {
408 a := planned
409 b := actual
410
411 // Our methodology here is a little tricky, to deal with the fact that
412 // it's impossible to directly correlate two non-equal set elements because
413 // they don't have identities separate from their values.
414 // The approach is to count the number of equivalent elements each element
415 // of a has in b and vice-versa, and then return true only if each element
416 // in both sets has at least one equivalent.
417 as := a.AsValueSlice()
418 bs := b.AsValueSlice()
419 aeqs := make([]bool, len(as))
420 beqs := make([]bool, len(bs))
421 for ai, av := range as {
422 for bi, bv := range bs {
423 if f(av, bv) {
424 aeqs[ai] = true
425 beqs[bi] = true
426 }
427 }
428 }
429
430 var errs []error
431 for i, eq := range aeqs {
432 if !eq {
433 errs = append(errs, path.NewErrorf("planned set element %#v does not correlate with any element in actual", as[i]))
434 }
435 }
436 if len(errs) > 0 {
437 // Exit early since otherwise we're likely to generate duplicate
438 // error messages from the other perspective in the subsequent loop.
439 return errs
440 }
441 for i, eq := range beqs {
442 if !eq {
443 errs = append(errs, path.NewErrorf("actual set element %#v does not correlate with any element in plan", bs[i]))
444 }
445 }
446 return errs
447 }