aboutsummaryrefslogtreecommitdiffhomepage
path: root/vendor/github.com/hashicorp/terraform/plans/objchange/compatible.go
blob: 8b7ef43fddfa0d27120cae228b7ba6cd0dd8de70 (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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
package objchange

import (
	"fmt"
	"strconv"

	"github.com/zclconf/go-cty/cty"
	"github.com/zclconf/go-cty/cty/convert"

	"github.com/hashicorp/terraform/configs/configschema"
)

// AssertObjectCompatible checks whether the given "actual" value is a valid
// completion of the possibly-partially-unknown "planned" value.
//
// This means that any known leaf value in "planned" must be equal to the
// corresponding value in "actual", and various other similar constraints.
//
// Any inconsistencies are reported by returning a non-zero number of errors.
// These errors are usually (but not necessarily) cty.PathError values
// referring to a particular nested value within the "actual" value.
//
// The two values must have types that conform to the given schema's implied
// type, or this function will panic.
func AssertObjectCompatible(schema *configschema.Block, planned, actual cty.Value) []error {
	return assertObjectCompatible(schema, planned, actual, nil)
}

func assertObjectCompatible(schema *configschema.Block, planned, actual cty.Value, path cty.Path) []error {
	var errs []error
	if planned.IsNull() && !actual.IsNull() {
		errs = append(errs, path.NewErrorf("was absent, but now present"))
		return errs
	}
	if actual.IsNull() && !planned.IsNull() {
		errs = append(errs, path.NewErrorf("was present, but now absent"))
		return errs
	}
	if planned.IsNull() {
		// No further checks possible if both values are null
		return errs
	}

	for name, attrS := range schema.Attributes {
		plannedV := planned.GetAttr(name)
		actualV := actual.GetAttr(name)

		path := append(path, cty.GetAttrStep{Name: name})
		moreErrs := assertValueCompatible(plannedV, actualV, path)
		if attrS.Sensitive {
			if len(moreErrs) > 0 {
				// Use a vague placeholder message instead, to avoid disclosing
				// sensitive information.
				errs = append(errs, path.NewErrorf("inconsistent values for sensitive attribute"))
			}
		} else {
			errs = append(errs, moreErrs...)
		}
	}
	for name, blockS := range schema.BlockTypes {
		plannedV := planned.GetAttr(name)
		actualV := actual.GetAttr(name)

		// As a special case, if there were any blocks whose leaf attributes
		// are all unknown then we assume (possibly incorrectly) that the
		// HCL dynamic block extension is in use with an unknown for_each
		// argument, and so we will do looser validation here that allows
		// for those blocks to have expanded into a different number of blocks
		// if the for_each value is now known.
		maybeUnknownBlocks := couldHaveUnknownBlockPlaceholder(plannedV, blockS, false)

		path := append(path, cty.GetAttrStep{Name: name})
		switch blockS.Nesting {
		case configschema.NestingSingle, configschema.NestingGroup:
			// If an unknown block placeholder was present then the placeholder
			// may have expanded out into zero blocks, which is okay.
			if maybeUnknownBlocks && actualV.IsNull() {
				continue
			}
			moreErrs := assertObjectCompatible(&blockS.Block, plannedV, actualV, path)
			errs = append(errs, moreErrs...)
		case configschema.NestingList:
			// A NestingList might either be a list or a tuple, depending on
			// whether there are dynamically-typed attributes inside. However,
			// both support a similar-enough API that we can treat them the
			// same for our purposes here.
			if !plannedV.IsKnown() || plannedV.IsNull() || actualV.IsNull() {
				continue
			}

			if maybeUnknownBlocks {
				// When unknown blocks are present the final blocks may be
				// at different indices than the planned blocks, so unfortunately
				// we can't do our usual checks in this case without generating
				// false negatives.
				continue
			}

			plannedL := plannedV.LengthInt()
			actualL := actualV.LengthInt()
			if plannedL != actualL {
				errs = append(errs, path.NewErrorf("block count changed from %d to %d", plannedL, actualL))
				continue
			}
			for it := plannedV.ElementIterator(); it.Next(); {
				idx, plannedEV := it.Element()
				if !actualV.HasIndex(idx).True() {
					continue
				}
				actualEV := actualV.Index(idx)
				moreErrs := assertObjectCompatible(&blockS.Block, plannedEV, actualEV, append(path, cty.IndexStep{Key: idx}))
				errs = append(errs, moreErrs...)
			}
		case configschema.NestingMap:
			// A NestingMap might either be a map or an object, depending on
			// whether there are dynamically-typed attributes inside, but
			// that's decided statically and so both values will have the same
			// kind.
			if plannedV.Type().IsObjectType() {
				plannedAtys := plannedV.Type().AttributeTypes()
				actualAtys := actualV.Type().AttributeTypes()
				for k := range plannedAtys {
					if _, ok := actualAtys[k]; !ok {
						errs = append(errs, path.NewErrorf("block key %q has vanished", k))
						continue
					}

					plannedEV := plannedV.GetAttr(k)
					actualEV := actualV.GetAttr(k)
					moreErrs := assertObjectCompatible(&blockS.Block, plannedEV, actualEV, append(path, cty.GetAttrStep{Name: k}))
					errs = append(errs, moreErrs...)
				}
				if !maybeUnknownBlocks { // new blocks may appear if unknown blocks were present in the plan
					for k := range actualAtys {
						if _, ok := plannedAtys[k]; !ok {
							errs = append(errs, path.NewErrorf("new block key %q has appeared", k))
							continue
						}
					}
				}
			} else {
				if !plannedV.IsKnown() || plannedV.IsNull() || actualV.IsNull() {
					continue
				}
				plannedL := plannedV.LengthInt()
				actualL := actualV.LengthInt()
				if plannedL != actualL && !maybeUnknownBlocks { // new blocks may appear if unknown blocks were persent in the plan
					errs = append(errs, path.NewErrorf("block count changed from %d to %d", plannedL, actualL))
					continue
				}
				for it := plannedV.ElementIterator(); it.Next(); {
					idx, plannedEV := it.Element()
					if !actualV.HasIndex(idx).True() {
						continue
					}
					actualEV := actualV.Index(idx)
					moreErrs := assertObjectCompatible(&blockS.Block, plannedEV, actualEV, append(path, cty.IndexStep{Key: idx}))
					errs = append(errs, moreErrs...)
				}
			}
		case configschema.NestingSet:
			if !plannedV.IsKnown() || !actualV.IsKnown() || plannedV.IsNull() || actualV.IsNull() {
				continue
			}

			setErrs := assertSetValuesCompatible(plannedV, actualV, path, func(plannedEV, actualEV cty.Value) bool {
				errs := assertObjectCompatible(&blockS.Block, plannedEV, actualEV, append(path, cty.IndexStep{Key: actualEV}))
				return len(errs) == 0
			})
			errs = append(errs, setErrs...)

			// There can be fewer elements in a set after its elements are all
			// known (values that turn out to be equal will coalesce) but the
			// number of elements must never get larger.
			plannedL := plannedV.LengthInt()
			actualL := actualV.LengthInt()
			if plannedL < actualL {
				errs = append(errs, path.NewErrorf("block set length changed from %d to %d", plannedL, actualL))
			}
		default:
			panic(fmt.Sprintf("unsupported nesting mode %s", blockS.Nesting))
		}
	}
	return errs
}

func assertValueCompatible(planned, actual cty.Value, path cty.Path) []error {
	// NOTE: We don't normally use the GoString rendering of cty.Value in
	// user-facing error messages as a rule, but we make an exception
	// for this function because we expect the user to pass this message on
	// verbatim to the provider development team and so more detail is better.

	var errs []error
	if planned.Type() == cty.DynamicPseudoType {
		// Anything goes, then
		return errs
	}
	if problems := planned.Type().TestConformance(actual.Type()); len(problems) > 0 {
		errs = append(errs, path.NewErrorf("wrong final value type: %s", convert.MismatchMessage(actual.Type(), planned.Type())))
		// If the types don't match then we can't do any other comparisons,
		// so we bail early.
		return errs
	}

	if !planned.IsKnown() {
		// We didn't know what were going to end up with during plan, so
		// anything goes during apply.
		return errs
	}

	if actual.IsNull() {
		if planned.IsNull() {
			return nil
		}
		errs = append(errs, path.NewErrorf("was %#v, but now null", planned))
		return errs
	}
	if planned.IsNull() {
		errs = append(errs, path.NewErrorf("was null, but now %#v", actual))
		return errs
	}

	ty := planned.Type()
	switch {

	case !actual.IsKnown():
		errs = append(errs, path.NewErrorf("was known, but now unknown"))

	case ty.IsPrimitiveType():
		if !actual.Equals(planned).True() {
			errs = append(errs, path.NewErrorf("was %#v, but now %#v", planned, actual))
		}

	case ty.IsListType() || ty.IsMapType() || ty.IsTupleType():
		for it := planned.ElementIterator(); it.Next(); {
			k, plannedV := it.Element()
			if !actual.HasIndex(k).True() {
				errs = append(errs, path.NewErrorf("element %s has vanished", indexStrForErrors(k)))
				continue
			}

			actualV := actual.Index(k)
			moreErrs := assertValueCompatible(plannedV, actualV, append(path, cty.IndexStep{Key: k}))
			errs = append(errs, moreErrs...)
		}

		for it := actual.ElementIterator(); it.Next(); {
			k, _ := it.Element()
			if !planned.HasIndex(k).True() {
				errs = append(errs, path.NewErrorf("new element %s has appeared", indexStrForErrors(k)))
			}
		}

	case ty.IsObjectType():
		atys := ty.AttributeTypes()
		for name := range atys {
			// Because we already tested that the two values have the same type,
			// we can assume that the same attributes are present in both and
			// focus just on testing their values.
			plannedV := planned.GetAttr(name)
			actualV := actual.GetAttr(name)
			moreErrs := assertValueCompatible(plannedV, actualV, append(path, cty.GetAttrStep{Name: name}))
			errs = append(errs, moreErrs...)
		}

	case ty.IsSetType():
		// We can't really do anything useful for sets here because changing
		// an unknown element to known changes the identity of the element, and
		// so we can't correlate them properly. However, we will at least check
		// to ensure that the number of elements is consistent, along with
		// the general type-match checks we ran earlier in this function.
		if planned.IsKnown() && !planned.IsNull() && !actual.IsNull() {

			setErrs := assertSetValuesCompatible(planned, actual, path, func(plannedV, actualV cty.Value) bool {
				errs := assertValueCompatible(plannedV, actualV, append(path, cty.IndexStep{Key: actualV}))
				return len(errs) == 0
			})
			errs = append(errs, setErrs...)

			// There can be fewer elements in a set after its elements are all
			// known (values that turn out to be equal will coalesce) but the
			// number of elements must never get larger.

			plannedL := planned.LengthInt()
			actualL := actual.LengthInt()
			if plannedL < actualL {
				errs = append(errs, path.NewErrorf("length changed from %d to %d", plannedL, actualL))
			}
		}
	}

	return errs
}

func indexStrForErrors(v cty.Value) string {
	switch v.Type() {
	case cty.Number:
		return v.AsBigFloat().Text('f', -1)
	case cty.String:
		return strconv.Quote(v.AsString())
	default:
		// Should be impossible, since no other index types are allowed!
		return fmt.Sprintf("%#v", v)
	}
}

// couldHaveUnknownBlockPlaceholder is a heuristic that recognizes how the
// HCL dynamic block extension behaves when it's asked to expand a block whose
// for_each argument is unknown. In such cases, it generates a single placeholder
// block with all leaf attribute values unknown, and once the for_each
// expression becomes known the placeholder may be replaced with any number
// of blocks, so object compatibility checks would need to be more liberal.
//
// Set "nested" if testing a block that is nested inside a candidate block
// placeholder; this changes the interpretation of there being no blocks of
// a type to allow for there being zero nested blocks.
func couldHaveUnknownBlockPlaceholder(v cty.Value, blockS *configschema.NestedBlock, nested bool) bool {
	switch blockS.Nesting {
	case configschema.NestingSingle, configschema.NestingGroup:
		if nested && v.IsNull() {
			return true // for nested blocks, a single block being unset doesn't disqualify from being an unknown block placeholder
		}
		return couldBeUnknownBlockPlaceholderElement(v, &blockS.Block)
	default:
		// These situations should be impossible for correct providers, but
		// we permit the legacy SDK to produce some incorrect outcomes
		// for compatibility with its existing logic, and so we must be
		// tolerant here.
		if !v.IsKnown() {
			return true
		}
		if v.IsNull() {
			return false // treated as if the list were empty, so we would see zero iterations below
		}

		// For all other nesting modes, our value should be something iterable.
		for it := v.ElementIterator(); it.Next(); {
			_, ev := it.Element()
			if couldBeUnknownBlockPlaceholderElement(ev, &blockS.Block) {
				return true
			}
		}

		// Our default changes depending on whether we're testing the candidate
		// block itself or something nested inside of it: zero blocks of a type
		// can never contain a dynamic block placeholder, but a dynamic block
		// placeholder might contain zero blocks of one of its own nested block
		// types, if none were set in the config at all.
		return nested
	}
}

func couldBeUnknownBlockPlaceholderElement(v cty.Value, schema *configschema.Block) bool {
	if v.IsNull() {
		return false // null value can never be a placeholder element
	}
	if !v.IsKnown() {
		return true // this should never happen for well-behaved providers, but can happen with the legacy SDK opt-outs
	}
	for name := range schema.Attributes {
		av := v.GetAttr(name)

		// Unknown block placeholders contain only unknown or null attribute
		// values, depending on whether or not a particular attribute was set
		// explicitly inside the content block. Note that this is imprecise:
		// non-placeholders can also match this, so this function can generate
		// false positives.
		if av.IsKnown() && !av.IsNull() {
			return false
		}
	}
	for name, blockS := range schema.BlockTypes {
		if !couldHaveUnknownBlockPlaceholder(v.GetAttr(name), blockS, true) {
			return false
		}
	}
	return true
}

// assertSetValuesCompatible checks that each of the elements in a can
// be correlated with at least one equivalent element in b and vice-versa,
// using the given correlation function.
//
// This allows the number of elements in the sets to change as long as all
// elements in both sets can be correlated, making this function safe to use
// with sets that may contain unknown values as long as the unknown case is
// addressed in some reasonable way in the callback function.
//
// The callback always recieves values from set a as its first argument and
// values from set b in its second argument, so it is safe to use with
// non-commutative functions.
//
// As with assertValueCompatible, we assume that the target audience of error
// messages here is a provider developer (via a bug report from a user) and so
// we intentionally violate our usual rule of keeping cty implementation
// details out of error messages.
func assertSetValuesCompatible(planned, actual cty.Value, path cty.Path, f func(aVal, bVal cty.Value) bool) []error {
	a := planned
	b := actual

	// 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 f(av, bv) {
				aeqs[ai] = true
				beqs[bi] = true
			}
		}
	}

	var errs []error
	for i, eq := range aeqs {
		if !eq {
			errs = append(errs, path.NewErrorf("planned set element %#v does not correlate with any element in actual", as[i]))
		}
	}
	if len(errs) > 0 {
		// Exit early since otherwise we're likely to generate duplicate
		// error messages from the other perspective in the subsequent loop.
		return errs
	}
	for i, eq := range beqs {
		if !eq {
			errs = append(errs, path.NewErrorf("actual set element %#v does not correlate with any element in plan", bs[i]))
		}
	}
	return errs
}