]> git.immae.eu Git - github/fretlink/terraform-provider-statuscake.git/blob - vendor/github.com/hashicorp/terraform/plans/objchange/compatible.go
Merge pull request #34 from jcalonso/fix/contact-group-backwards-compatible
[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() || 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 }