-
Notifications
You must be signed in to change notification settings - Fork 9.4k
/
objchange.go
494 lines (417 loc) · 15.8 KB
/
objchange.go
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
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package objchange
import (
"errors"
"fmt"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/configs/configschema"
)
// ProposedNew constructs a proposed new object value by combining the
// computed attribute values from "prior" with the configured attribute values
// from "config".
//
// Both value must conform to the given schema's implied type, or this function
// will panic.
//
// The prior value must be wholly known, but the config value may be unknown
// or have nested unknown values.
//
// The merging of the two objects includes the attributes of any nested blocks,
// which will be correlated in a manner appropriate for their nesting mode.
// Note in particular that the correlation for blocks backed by sets is a
// heuristic based on matching non-computed attribute values and so it may
// produce strange results with more "extreme" cases, such as a nested set
// block where _all_ attributes are computed.
func ProposedNew(schema *configschema.Block, prior, config cty.Value) cty.Value {
// If the config and prior are both null, return early here before
// populating the prior block. The prevents non-null blocks from appearing
// the proposed state value.
if config.IsNull() && prior.IsNull() {
return prior
}
if prior.IsNull() {
// In this case, we will construct a synthetic prior value that is
// similar to the result of decoding an empty configuration block,
// which simplifies our handling of the top-level attributes/blocks
// below by giving us one non-null level of object to pull values from.
//
// "All attributes null" happens to be the definition of EmptyValue for
// a Block, so we can just delegate to that
prior = schema.EmptyValue()
}
return proposedNew(schema, prior, config)
}
// PlannedDataResourceObject is similar to proposedNewBlock but tailored for
// planning data resources in particular. Specifically, it replaces the values
// of any Computed attributes not set in the configuration with an unknown
// value, which serves as a placeholder for a value to be filled in by the
// provider when the data resource is finally read.
//
// Data resources are different because the planning of them is handled
// entirely within Terraform Core and not subject to customization by the
// provider. This function is, in effect, producing an equivalent result to
// passing the proposedNewBlock result into a provider's PlanResourceChange
// function, assuming a fixed implementation of PlanResourceChange that just
// fills in unknown values as needed.
func PlannedDataResourceObject(schema *configschema.Block, config cty.Value) cty.Value {
// Our trick here is to run the proposedNewBlock logic with an
// entirely-unknown prior value. Because of cty's unknown short-circuit
// behavior, any operation on prior returns another unknown, and so
// unknown values propagate into all of the parts of the resulting value
// that would normally be filled in by preserving the prior state.
prior := cty.UnknownVal(schema.ImpliedType())
return proposedNew(schema, prior, config)
}
func proposedNew(schema *configschema.Block, prior, config cty.Value) cty.Value {
if config.IsNull() || !config.IsKnown() {
// A block config should never be null at this point. The only nullable
// block type is NestingSingle, which will return early before coming
// back here. We'll allow the null here anyway to free callers from
// needing to specifically check for these cases, and any mismatch will
// be caught in validation, so just take the prior value rather than
// the invalid null.
return prior
}
if (!prior.Type().IsObjectType()) || (!config.Type().IsObjectType()) {
panic("ProposedNew only supports object-typed values")
}
// From this point onwards, we can assume that both values are non-null
// object types, and that the config value itself is known (though it
// may contain nested values that are unknown.)
newAttrs := proposedNewAttributes(schema.Attributes, prior, config)
// Merging nested blocks is a little more complex, since we need to
// correlate blocks between both objects and then recursively propose
// a new object for each. The correlation logic depends on the nesting
// mode for each block type.
for name, blockType := range schema.BlockTypes {
priorV := prior.GetAttr(name)
configV := config.GetAttr(name)
newAttrs[name] = proposedNewNestedBlock(blockType, priorV, configV)
}
return cty.ObjectVal(newAttrs)
}
// proposedNewBlockOrObject dispatched the schema to either ProposedNew or
// proposedNewObjectAttributes depending on the given type.
func proposedNewBlockOrObject(schema nestedSchema, prior, config cty.Value) cty.Value {
switch schema := schema.(type) {
case *configschema.Block:
return ProposedNew(schema, prior, config)
case *configschema.Object:
return proposedNewObjectAttributes(schema, prior, config)
default:
panic(fmt.Sprintf("unexpected schema type %T", schema))
}
}
func proposedNewNestedBlock(schema *configschema.NestedBlock, prior, config cty.Value) cty.Value {
// The only time we should encounter an entirely unknown block is from the
// use of dynamic with an unknown for_each expression.
if !config.IsKnown() {
return config
}
newV := config
switch schema.Nesting {
case configschema.NestingSingle:
// A NestingSingle configuration block value can be null, and since it
// cannot be computed we can always take the configuration value.
if config.IsNull() {
break
}
// Otherwise use the same assignment rules as NestingGroup
fallthrough
case configschema.NestingGroup:
newV = ProposedNew(&schema.Block, prior, config)
case configschema.NestingList:
newV = proposedNewNestingList(&schema.Block, prior, config)
case configschema.NestingMap:
newV = proposedNewNestingMap(&schema.Block, prior, config)
case configschema.NestingSet:
newV = proposedNewNestingSet(&schema.Block, prior, config)
default:
// Should never happen, since the above cases are comprehensive.
panic(fmt.Sprintf("unsupported block nesting mode %s", schema.Nesting))
}
return newV
}
func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value) cty.Value {
// if the config isn't known at all, then we must use that value
if !config.IsKnown() {
return config
}
// Even if the config is null or empty, we will be using this default value.
newV := config
switch schema.Nesting {
case configschema.NestingSingle:
// If the config is null, we already have our value. If the attribute
// is optional+computed, we won't reach this branch with a null value
// since the computed case would have been taken.
if config.IsNull() {
break
}
newV = proposedNewObjectAttributes(schema, prior, config)
case configschema.NestingList:
newV = proposedNewNestingList(schema, prior, config)
case configschema.NestingMap:
newV = proposedNewNestingMap(schema, prior, config)
case configschema.NestingSet:
newV = proposedNewNestingSet(schema, prior, config)
default:
// Should never happen, since the above cases are comprehensive.
panic(fmt.Sprintf("unsupported attribute nesting mode %s", schema.Nesting))
}
return newV
}
func proposedNewNestingList(schema nestedSchema, prior, config cty.Value) cty.Value {
newV := config
// Nested blocks are correlated by index.
configVLen := 0
if !config.IsNull() {
configVLen = config.LengthInt()
}
if configVLen > 0 {
newVals := make([]cty.Value, 0, configVLen)
for it := config.ElementIterator(); it.Next(); {
idx, configEV := it.Element()
if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) {
// If there is no corresponding prior element then
// we just take the config value as-is.
newVals = append(newVals, configEV)
continue
}
priorEV := prior.Index(idx)
newVals = append(newVals, proposedNewBlockOrObject(schema, priorEV, configEV))
}
// Despite the name, a NestingList might also be a tuple, if
// its nested schema contains dynamically-typed attributes.
if config.Type().IsTupleType() {
newV = cty.TupleVal(newVals)
} else {
newV = cty.ListVal(newVals)
}
}
return newV
}
func proposedNewNestingMap(schema nestedSchema, prior, config cty.Value) cty.Value {
newV := config
newVals := map[string]cty.Value{}
if config.IsNull() || !config.IsKnown() || config.LengthInt() == 0 {
// We already assigned newVal and there's nothing to compare in
// config.
return newV
}
cfgMap := config.AsValueMap()
// prior may be null or empty
priorMap := map[string]cty.Value{}
if !prior.IsNull() && prior.IsKnown() && prior.LengthInt() > 0 {
priorMap = prior.AsValueMap()
}
for name, configEV := range cfgMap {
priorEV, inPrior := priorMap[name]
if !inPrior {
// If there is no corresponding prior element then
// we just take the config value as-is.
newVals[name] = configEV
continue
}
newVals[name] = proposedNewBlockOrObject(schema, priorEV, configEV)
}
// The value must leave as the same type it came in as
switch {
case config.Type().IsObjectType():
// Although we call the nesting mode "map", we actually use
// object values so that elements might have different types
// in case of dynamically-typed attributes.
newV = cty.ObjectVal(newVals)
default:
newV = cty.MapVal(newVals)
}
return newV
}
func proposedNewNestingSet(schema nestedSchema, prior, config cty.Value) cty.Value {
if !config.Type().IsSetType() {
panic("configschema.NestingSet value is not a set as expected")
}
newV := config
if !config.IsKnown() || config.IsNull() || config.LengthInt() == 0 {
return newV
}
var priorVals []cty.Value
if prior.IsKnown() && !prior.IsNull() {
priorVals = prior.AsValueSlice()
}
var newVals []cty.Value
// track which prior elements have been used
used := make([]bool, len(priorVals))
for _, configEV := range config.AsValueSlice() {
var priorEV cty.Value
for i, priorCmp := range priorVals {
if used[i] {
continue
}
// It is possible that multiple prior elements could be valid
// matches for a configuration value, in which case we will end up
// picking the first match encountered (but it will always be
// consistent due to cty's iteration order). Because configured set
// elements must also be entirely unique in order to be included in
// the set, these matches either will not matter because they only
// differ by computed values, or could not have come from a valid
// config with all unique set elements.
if validPriorFromConfig(schema, priorCmp, configEV) {
priorEV = priorCmp
used[i] = true
break
}
}
if priorEV == cty.NilVal {
priorEV = cty.NullVal(config.Type().ElementType())
}
newVals = append(newVals, proposedNewBlockOrObject(schema, priorEV, configEV))
}
return cty.SetVal(newVals)
}
func proposedNewObjectAttributes(schema *configschema.Object, prior, config cty.Value) cty.Value {
if config.IsNull() {
return config
}
return cty.ObjectVal(proposedNewAttributes(schema.Attributes, prior, config))
}
func proposedNewAttributes(attrs map[string]*configschema.Attribute, prior, config cty.Value) map[string]cty.Value {
newAttrs := make(map[string]cty.Value, len(attrs))
for name, attr := range attrs {
var priorV cty.Value
if prior.IsNull() {
priorV = cty.NullVal(prior.Type().AttributeType(name))
} else {
priorV = prior.GetAttr(name)
}
configV := config.GetAttr(name)
var newV cty.Value
switch {
// required isn't considered when constructing the plan, so attributes
// are essentially either computed or not computed. In the case of
// optional+computed, they are only computed when there is no
// configuration.
case attr.Computed && configV.IsNull():
// configV will always be null in this case, by definition.
// priorV may also be null, but that's okay.
newV = priorV
// the exception to the above is that if the config is optional and
// the _prior_ value contains non-computed values, we can infer
// that the config must have been non-null previously.
if optionalValueNotComputable(attr, priorV) {
newV = configV
}
case attr.NestedType != nil:
// For non-computed NestedType attributes, we need to descend
// into the individual nested attributes to build the final
// value, unless the entire nested attribute is unknown.
newV = proposedNewNestedType(attr.NestedType, priorV, configV)
default:
// For non-computed attributes, we always take the config value,
// even if it is null. If it's _required_ then null values
// should've been caught during an earlier validation step, and
// so we don't really care about that here.
newV = configV
}
newAttrs[name] = newV
}
return newAttrs
}
// nestedSchema is used as a generic container for either a
// *configschema.Object, or *configschema.Block.
type nestedSchema interface {
AttributeByPath(cty.Path) *configschema.Attribute
}
// optionalValueNotComputable is used to check if an object in state must
// have at least partially come from configuration. If the prior value has any
// non-null attributes which are not computed in the schema, then we know there
// was previously a configuration value which set those.
//
// This is used when the configuration contains a null optional+computed value,
// and we want to know if we should plan to send the null value or the prior
// state.
func optionalValueNotComputable(schema *configschema.Attribute, val cty.Value) bool {
if !schema.Optional {
return false
}
// We must have a NestedType for complex nested attributes in order
// to find nested computed values in the first place.
if schema.NestedType == nil {
return false
}
foundNonComputedAttr := false
cty.Walk(val, func(path cty.Path, v cty.Value) (bool, error) {
if v.IsNull() {
return true, nil
}
attr := schema.NestedType.AttributeByPath(path)
if attr == nil {
return true, nil
}
if !attr.Computed {
foundNonComputedAttr = true
return false, nil
}
return true, nil
})
return foundNonComputedAttr
}
// validPriorFromConfig returns true if the prior object could have been
// derived from the configuration. We do this by walking the prior value to
// determine if it is a valid superset of the config, and only computable
// values have been added. This function is only used to correlated
// configuration with possible valid prior values within sets.
func validPriorFromConfig(schema nestedSchema, prior, config cty.Value) bool {
if config.RawEquals(prior) {
return true
}
// error value to halt the walk
stop := errors.New("stop")
valid := true
cty.Walk(prior, func(path cty.Path, priorV cty.Value) (bool, error) {
configV, err := path.Apply(config)
if err != nil {
// most likely dynamic objects with different types
valid = false
return false, stop
}
// we don't need to know the schema if both are equal
if configV.RawEquals(priorV) {
// we know they are equal, so no need to descend further
return false, nil
}
// We can't descend into nested sets to correlate configuration, so the
// overall values must be equal.
if configV.Type().IsSetType() {
valid = false
return false, stop
}
attr := schema.AttributeByPath(path)
if attr == nil {
// Not at a schema attribute, so we can continue until we find leaf
// attributes.
return true, nil
}
// If we have nested object attributes we'll be descending into those
// to compare the individual values and determine why this level is not
// equal
if attr.NestedType != nil {
return true, nil
}
// This is a leaf attribute, so it must be computed in order to differ
// from config.
if !attr.Computed {
valid = false
return false, stop
}
// And if it is computed, the config must be null to allow a change.
if !configV.IsNull() {
valid = false
return false, stop
}
// We sill stop here. The cty value could be far larger, but this was
// the last level of prescribed schema.
return false, nil
})
return valid
}