-
Notifications
You must be signed in to change notification settings - Fork 9.6k
/
resource_diff.go
468 lines (403 loc) · 13.8 KB
/
resource_diff.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
package schema
import (
"errors"
"fmt"
"reflect"
"strings"
"sync"
"github.com/hashicorp/terraform/terraform"
)
// newValueWriter is a minor re-implementation of MapFieldWriter to include
// keys that should be marked as computed, to represent the new part of a
// pseudo-diff.
type newValueWriter struct {
*MapFieldWriter
// A list of keys that should be marked as computed.
computedKeys map[string]bool
// A lock to prevent races on writes. The underlying writer will have one as
// well - this is for computed keys.
lock sync.Mutex
// To be used with init.
once sync.Once
}
// init performs any initialization tasks for the newValueWriter.
func (w *newValueWriter) init() {
if w.computedKeys == nil {
w.computedKeys = make(map[string]bool)
}
}
// WriteField overrides MapValueWriter's WriteField, adding the ability to flag
// the address as computed.
func (w *newValueWriter) WriteField(address []string, value interface{}, computed bool) error {
// Fail the write if we have a non-nil value and computed is true.
// NewComputed values should not have a value when written.
if value != nil && computed {
return errors.New("Non-nil value with computed set")
}
if err := w.MapFieldWriter.WriteField(address, value); err != nil {
return err
}
w.once.Do(w.init)
w.lock.Lock()
defer w.lock.Unlock()
if computed {
w.computedKeys[strings.Join(address, ".")] = true
}
return nil
}
// ComputedKeysMap returns the underlying computed keys map.
func (w *newValueWriter) ComputedKeysMap() map[string]bool {
w.once.Do(w.init)
return w.computedKeys
}
// newValueReader is a minor re-implementation of MapFieldReader and is the
// read counterpart to MapValueWriter, allowing the read of keys flagged as
// computed to accommodate the diff override logic in ResourceDiff.
type newValueReader struct {
*MapFieldReader
// The list of computed keys from a newValueWriter.
computedKeys map[string]bool
}
// ReadField reads the values from the underlying writer, returning the
// computed value if it is found as well.
func (r *newValueReader) ReadField(address []string) (FieldReadResult, error) {
addrKey := strings.Join(address, ".")
v, err := r.MapFieldReader.ReadField(address)
if err != nil {
return FieldReadResult{}, err
}
for computedKey := range r.computedKeys {
if childAddrOf(addrKey, computedKey) {
if strings.HasSuffix(addrKey, ".#") {
// This is a count value for a list or set that has been marked as
// computed, or a sub-list/sub-set of a complex resource that has
// been marked as computed. We need to pass through to other readers
// so that an accurate previous count can be fetched for the diff.
v.Exists = false
}
v.Computed = true
}
}
return v, nil
}
// ResourceDiff is used to query and make custom changes to an in-flight diff.
// It can be used to veto particular changes in the diff, customize the diff
// that has been created, or diff values not controlled by config.
//
// The object functions similar to ResourceData, however most notably lacks
// Set, SetPartial, and Partial, as it should be used to change diff values
// only. Most other first-class ResourceData functions exist, namely Get,
// GetOk, HasChange, and GetChange exist.
//
// All functions in ResourceDiff, save for ForceNew, can only be used on
// computed fields.
type ResourceDiff struct {
// The schema for the resource being worked on.
schema map[string]*Schema
// The current config for this resource.
config *terraform.ResourceConfig
// The state for this resource as it exists post-refresh, after the initial
// diff.
state *terraform.InstanceState
// The diff created by Terraform. This diff is used, along with state,
// config, and custom-set diff data, to provide a multi-level reader
// experience similar to ResourceData.
diff *terraform.InstanceDiff
// The internal reader structure that contains the state, config, the default
// diff, and the new diff.
multiReader *MultiLevelFieldReader
// A writer that writes overridden new fields.
newWriter *newValueWriter
// Tracks which keys have been updated by ResourceDiff to ensure that the
// diff does not get re-run on keys that were not touched, or diffs that were
// just removed (re-running on the latter would just roll back the removal).
updatedKeys map[string]bool
}
// newResourceDiff creates a new ResourceDiff instance.
func newResourceDiff(schema map[string]*Schema, config *terraform.ResourceConfig, state *terraform.InstanceState, diff *terraform.InstanceDiff) *ResourceDiff {
d := &ResourceDiff{
config: config,
state: state,
diff: diff,
schema: schema,
}
d.newWriter = &newValueWriter{
MapFieldWriter: &MapFieldWriter{Schema: d.schema},
}
readers := make(map[string]FieldReader)
var stateAttributes map[string]string
if d.state != nil {
stateAttributes = d.state.Attributes
readers["state"] = &MapFieldReader{
Schema: d.schema,
Map: BasicMapReader(stateAttributes),
}
}
if d.config != nil {
readers["config"] = &ConfigFieldReader{
Schema: d.schema,
Config: d.config,
}
}
if d.diff != nil {
readers["diff"] = &DiffFieldReader{
Schema: d.schema,
Diff: d.diff,
Source: &MultiLevelFieldReader{
Levels: []string{"state", "config"},
Readers: readers,
},
}
}
readers["newDiff"] = &newValueReader{
MapFieldReader: &MapFieldReader{
Schema: d.schema,
Map: BasicMapReader(d.newWriter.Map()),
},
computedKeys: d.newWriter.ComputedKeysMap(),
}
d.multiReader = &MultiLevelFieldReader{
Levels: []string{
"state",
"config",
"diff",
"newDiff",
},
Readers: readers,
}
d.updatedKeys = make(map[string]bool)
return d
}
// UpdatedKeys returns the keys that were updated by this ResourceDiff run.
// These are the only keys that a diff should be re-calculated for.
func (d *ResourceDiff) UpdatedKeys() []string {
var s []string
for k := range d.updatedKeys {
s = append(s, k)
}
return s
}
// Clear wipes the diff for a particular key. It is called by ResourceDiff's
// functionality to remove any possibility of conflicts, but can be called on
// its own to just remove a specific key from the diff completely.
//
// Note that this does not wipe an override. This function is only allowed on
// computed keys.
func (d *ResourceDiff) Clear(key string) error {
if err := d.checkKey(key, "Clear"); err != nil {
return err
}
return d.clear(key)
}
func (d *ResourceDiff) clear(key string) error {
// Check the schema to make sure that this key exists first.
if _, ok := d.schema[key]; !ok {
return fmt.Errorf("%s is not a valid key", key)
}
for k := range d.diff.Attributes {
if strings.HasPrefix(k, key) {
delete(d.diff.Attributes, k)
}
}
return nil
}
// diffChange helps to implement resourceDiffer and derives its change values
// from ResourceDiff's own change data, in addition to existing diff, config, and state.
func (d *ResourceDiff) diffChange(key string) (interface{}, interface{}, bool, bool, bool) {
old, new, customized := d.getChange(key)
if !old.Exists {
old.Value = nil
}
if !new.Exists {
new.Value = nil
}
return old.Value, new.Value, !reflect.DeepEqual(old.Value, new.Value), new.Computed, customized
}
// SetNew is used to set a new diff value for the mentioned key. The value must
// be correct for the attribute's schema (mostly relevant for maps, lists, and
// sets). The original value from the state is used as the old value.
//
// This function is only allowed on computed attributes.
func (d *ResourceDiff) SetNew(key string, value interface{}) error {
if err := d.checkKey(key, "SetNew"); err != nil {
return err
}
return d.setDiff(key, value, false)
}
// SetNewComputed functions like SetNew, except that it blanks out a new value
// and marks it as computed.
//
// This function is only allowed on computed attributes.
func (d *ResourceDiff) SetNewComputed(key string) error {
if err := d.checkKey(key, "SetNewComputed"); err != nil {
return err
}
return d.setDiff(key, nil, true)
}
// setDiff performs common diff setting behaviour.
func (d *ResourceDiff) setDiff(key string, new interface{}, computed bool) error {
if err := d.clear(key); err != nil {
return err
}
if err := d.newWriter.WriteField(strings.Split(key, "."), new, computed); err != nil {
return fmt.Errorf("Cannot set new diff value for key %s: %s", key, err)
}
d.updatedKeys[key] = true
return nil
}
// ForceNew force-flags ForceNew in the schema for a specific key, and
// re-calculates its diff, effectively causing this attribute to force a new
// resource.
//
// Keep in mind that forcing a new resource will force a second run of the
// resource's CustomizeDiff function (with a new ResourceDiff) once the current
// one has completed. This second run is performed without state. This behavior
// will be the same as if a new resource is being created and is performed to
// ensure that the diff looks like the diff for a new resource as much as
// possible. CustomizeDiff should expect such a scenario and act correctly.
//
// This function is a no-op/error if there is no diff.
//
// Note that the change to schema is permanent for the lifecycle of this
// specific ResourceDiff instance.
func (d *ResourceDiff) ForceNew(key string) error {
if !d.HasChange(key) {
return fmt.Errorf("ForceNew: No changes for %s", key)
}
_, new := d.GetChange(key)
d.schema[key].ForceNew = true
return d.setDiff(key, new, false)
}
// Get hands off to ResourceData.Get.
func (d *ResourceDiff) Get(key string) interface{} {
r, _ := d.GetOk(key)
return r
}
// GetChange gets the change between the state and diff, checking first to see
// if a overridden diff exists.
//
// This implementation differs from ResourceData's in the way that we first get
// results from the exact levels for the new diff, then from state and diff as
// per normal.
func (d *ResourceDiff) GetChange(key string) (interface{}, interface{}) {
old, new, _ := d.getChange(key)
return old.Value, new.Value
}
// GetOk functions the same way as ResourceData.GetOk, but it also checks the
// new diff levels to provide data consistent with the current state of the
// customized diff.
func (d *ResourceDiff) GetOk(key string) (interface{}, bool) {
r := d.get(strings.Split(key, "."), "newDiff")
exists := r.Exists && !r.Computed
if exists {
// If it exists, we also want to verify it is not the zero-value.
value := r.Value
zero := r.Schema.Type.Zero()
if eq, ok := value.(Equal); ok {
exists = !eq.Equal(zero)
} else {
exists = !reflect.DeepEqual(value, zero)
}
}
return r.Value, exists
}
// HasChange checks to see if there is a change between state and the diff, or
// in the overridden diff.
func (d *ResourceDiff) HasChange(key string) bool {
old, new := d.GetChange(key)
// If the type implements the Equal interface, then call that
// instead of just doing a reflect.DeepEqual. An example where this is
// needed is *Set
if eq, ok := old.(Equal); ok {
return !eq.Equal(new)
}
return !reflect.DeepEqual(old, new)
}
// Id returns the ID of this resource.
//
// Note that technically, ID does not change during diffs (it either has
// already changed in the refresh, or will change on update), hence we do not
// support updating the ID or fetching it from anything else other than state.
func (d *ResourceDiff) Id() string {
var result string
if d.state != nil {
result = d.state.ID
}
return result
}
// getChange gets values from two different levels, designed for use in
// diffChange, HasChange, and GetChange.
//
// This implementation differs from ResourceData's in the way that we first get
// results from the exact levels for the new diff, then from state and diff as
// per normal.
func (d *ResourceDiff) getChange(key string) (getResult, getResult, bool) {
old := d.get(strings.Split(key, "."), "state")
var new getResult
for p := range d.updatedKeys {
if childAddrOf(key, p) {
new = d.getExact(strings.Split(key, "."), "newDiff")
return old, new, true
}
}
new = d.get(strings.Split(key, "."), "newDiff")
return old, new, false
}
// get performs the appropriate multi-level reader logic for ResourceDiff,
// starting at source. Refer to newResourceDiff for the level order.
func (d *ResourceDiff) get(addr []string, source string) getResult {
result, err := d.multiReader.ReadFieldMerge(addr, source)
if err != nil {
panic(err)
}
return d.finalizeResult(addr, result)
}
// getExact gets an attribute from the exact level referenced by source.
func (d *ResourceDiff) getExact(addr []string, source string) getResult {
result, err := d.multiReader.ReadFieldExact(addr, source)
if err != nil {
panic(err)
}
return d.finalizeResult(addr, result)
}
// finalizeResult does some post-processing of the result produced by get and getExact.
func (d *ResourceDiff) finalizeResult(addr []string, result FieldReadResult) getResult {
// If the result doesn't exist, then we set the value to the zero value
var schema *Schema
if schemaL := addrToSchema(addr, d.schema); len(schemaL) > 0 {
schema = schemaL[len(schemaL)-1]
}
if result.Value == nil && schema != nil {
result.Value = result.ValueOrZero(schema)
}
// Transform the FieldReadResult into a getResult. It might be worth
// merging these two structures one day.
return getResult{
Value: result.Value,
ValueProcessed: result.ValueProcessed,
Computed: result.Computed,
Exists: result.Exists,
Schema: schema,
}
}
// childAddrOf does a comparison of two addresses to see if one is the child of
// the other.
func childAddrOf(child, parent string) bool {
cs := strings.Split(child, ".")
ps := strings.Split(parent, ".")
if len(ps) > len(cs) {
return false
}
return reflect.DeepEqual(ps, cs[:len(ps)])
}
// checkKey checks the key to make sure it exists and is computed.
func (d *ResourceDiff) checkKey(key, caller string) error {
s, ok := d.schema[key]
if !ok {
return fmt.Errorf("%s: invalid key: %s", caller, key)
}
if !s.Computed {
return fmt.Errorf("%s only operates on computed keys - %s is not one", caller, key)
}
return nil
}