github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/legacy/helper/schema/resource_diff.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package schema 5 6 import ( 7 "errors" 8 "fmt" 9 "reflect" 10 "strings" 11 "sync" 12 13 "github.com/terramate-io/tf/legacy/terraform" 14 ) 15 16 // newValueWriter is a minor re-implementation of MapFieldWriter to include 17 // keys that should be marked as computed, to represent the new part of a 18 // pseudo-diff. 19 type newValueWriter struct { 20 *MapFieldWriter 21 22 // A list of keys that should be marked as computed. 23 computedKeys map[string]bool 24 25 // A lock to prevent races on writes. The underlying writer will have one as 26 // well - this is for computed keys. 27 lock sync.Mutex 28 29 // To be used with init. 30 once sync.Once 31 } 32 33 // init performs any initialization tasks for the newValueWriter. 34 func (w *newValueWriter) init() { 35 if w.computedKeys == nil { 36 w.computedKeys = make(map[string]bool) 37 } 38 } 39 40 // WriteField overrides MapValueWriter's WriteField, adding the ability to flag 41 // the address as computed. 42 func (w *newValueWriter) WriteField(address []string, value interface{}, computed bool) error { 43 // Fail the write if we have a non-nil value and computed is true. 44 // NewComputed values should not have a value when written. 45 if value != nil && computed { 46 return errors.New("Non-nil value with computed set") 47 } 48 49 if err := w.MapFieldWriter.WriteField(address, value); err != nil { 50 return err 51 } 52 53 w.once.Do(w.init) 54 55 w.lock.Lock() 56 defer w.lock.Unlock() 57 if computed { 58 w.computedKeys[strings.Join(address, ".")] = true 59 } 60 return nil 61 } 62 63 // ComputedKeysMap returns the underlying computed keys map. 64 func (w *newValueWriter) ComputedKeysMap() map[string]bool { 65 w.once.Do(w.init) 66 return w.computedKeys 67 } 68 69 // newValueReader is a minor re-implementation of MapFieldReader and is the 70 // read counterpart to MapValueWriter, allowing the read of keys flagged as 71 // computed to accommodate the diff override logic in ResourceDiff. 72 type newValueReader struct { 73 *MapFieldReader 74 75 // The list of computed keys from a newValueWriter. 76 computedKeys map[string]bool 77 } 78 79 // ReadField reads the values from the underlying writer, returning the 80 // computed value if it is found as well. 81 func (r *newValueReader) ReadField(address []string) (FieldReadResult, error) { 82 addrKey := strings.Join(address, ".") 83 v, err := r.MapFieldReader.ReadField(address) 84 if err != nil { 85 return FieldReadResult{}, err 86 } 87 for computedKey := range r.computedKeys { 88 if childAddrOf(addrKey, computedKey) { 89 if strings.HasSuffix(addrKey, ".#") { 90 // This is a count value for a list or set that has been marked as 91 // computed, or a sub-list/sub-set of a complex resource that has 92 // been marked as computed. We need to pass through to other readers 93 // so that an accurate previous count can be fetched for the diff. 94 v.Exists = false 95 } 96 v.Computed = true 97 } 98 } 99 100 return v, nil 101 } 102 103 // ResourceDiff is used to query and make custom changes to an in-flight diff. 104 // It can be used to veto particular changes in the diff, customize the diff 105 // that has been created, or diff values not controlled by config. 106 // 107 // The object functions similar to ResourceData, however most notably lacks 108 // Set, SetPartial, and Partial, as it should be used to change diff values 109 // only. Most other first-class ResourceData functions exist, namely Get, 110 // GetOk, HasChange, and GetChange exist. 111 // 112 // All functions in ResourceDiff, save for ForceNew, can only be used on 113 // computed fields. 114 type ResourceDiff struct { 115 // The schema for the resource being worked on. 116 schema map[string]*Schema 117 118 // The current config for this resource. 119 config *terraform.ResourceConfig 120 121 // The state for this resource as it exists post-refresh, after the initial 122 // diff. 123 state *terraform.InstanceState 124 125 // The diff created by Terraform. This diff is used, along with state, 126 // config, and custom-set diff data, to provide a multi-level reader 127 // experience similar to ResourceData. 128 diff *terraform.InstanceDiff 129 130 // The internal reader structure that contains the state, config, the default 131 // diff, and the new diff. 132 multiReader *MultiLevelFieldReader 133 134 // A writer that writes overridden new fields. 135 newWriter *newValueWriter 136 137 // Tracks which keys have been updated by ResourceDiff to ensure that the 138 // diff does not get re-run on keys that were not touched, or diffs that were 139 // just removed (re-running on the latter would just roll back the removal). 140 updatedKeys map[string]bool 141 142 // Tracks which keys were flagged as forceNew. These keys are not saved in 143 // newWriter, but we need to track them so that they can be re-diffed later. 144 forcedNewKeys map[string]bool 145 } 146 147 // newResourceDiff creates a new ResourceDiff instance. 148 func newResourceDiff(schema map[string]*Schema, config *terraform.ResourceConfig, state *terraform.InstanceState, diff *terraform.InstanceDiff) *ResourceDiff { 149 d := &ResourceDiff{ 150 config: config, 151 state: state, 152 diff: diff, 153 schema: schema, 154 } 155 156 d.newWriter = &newValueWriter{ 157 MapFieldWriter: &MapFieldWriter{Schema: d.schema}, 158 } 159 readers := make(map[string]FieldReader) 160 var stateAttributes map[string]string 161 if d.state != nil { 162 stateAttributes = d.state.Attributes 163 readers["state"] = &MapFieldReader{ 164 Schema: d.schema, 165 Map: BasicMapReader(stateAttributes), 166 } 167 } 168 if d.config != nil { 169 readers["config"] = &ConfigFieldReader{ 170 Schema: d.schema, 171 Config: d.config, 172 } 173 } 174 if d.diff != nil { 175 readers["diff"] = &DiffFieldReader{ 176 Schema: d.schema, 177 Diff: d.diff, 178 Source: &MultiLevelFieldReader{ 179 Levels: []string{"state", "config"}, 180 Readers: readers, 181 }, 182 } 183 } 184 readers["newDiff"] = &newValueReader{ 185 MapFieldReader: &MapFieldReader{ 186 Schema: d.schema, 187 Map: BasicMapReader(d.newWriter.Map()), 188 }, 189 computedKeys: d.newWriter.ComputedKeysMap(), 190 } 191 d.multiReader = &MultiLevelFieldReader{ 192 Levels: []string{ 193 "state", 194 "config", 195 "diff", 196 "newDiff", 197 }, 198 199 Readers: readers, 200 } 201 202 d.updatedKeys = make(map[string]bool) 203 d.forcedNewKeys = make(map[string]bool) 204 205 return d 206 } 207 208 // UpdatedKeys returns the keys that were updated by this ResourceDiff run. 209 // These are the only keys that a diff should be re-calculated for. 210 // 211 // This is the combined result of both keys for which diff values were updated 212 // for or cleared, and also keys that were flagged to be re-diffed as a result 213 // of ForceNew. 214 func (d *ResourceDiff) UpdatedKeys() []string { 215 var s []string 216 for k := range d.updatedKeys { 217 s = append(s, k) 218 } 219 for k := range d.forcedNewKeys { 220 for _, l := range s { 221 if k == l { 222 break 223 } 224 } 225 s = append(s, k) 226 } 227 return s 228 } 229 230 // Clear wipes the diff for a particular key. It is called by ResourceDiff's 231 // functionality to remove any possibility of conflicts, but can be called on 232 // its own to just remove a specific key from the diff completely. 233 // 234 // Note that this does not wipe an override. This function is only allowed on 235 // computed keys. 236 func (d *ResourceDiff) Clear(key string) error { 237 if err := d.checkKey(key, "Clear", true); err != nil { 238 return err 239 } 240 241 return d.clear(key) 242 } 243 244 func (d *ResourceDiff) clear(key string) error { 245 // Check the schema to make sure that this key exists first. 246 schemaL := addrToSchema(strings.Split(key, "."), d.schema) 247 if len(schemaL) == 0 { 248 return fmt.Errorf("%s is not a valid key", key) 249 } 250 251 for k := range d.diff.Attributes { 252 if strings.HasPrefix(k, key) { 253 delete(d.diff.Attributes, k) 254 } 255 } 256 return nil 257 } 258 259 // GetChangedKeysPrefix helps to implement Resource.CustomizeDiff 260 // where we need to act on all nested fields 261 // without calling out each one separately 262 func (d *ResourceDiff) GetChangedKeysPrefix(prefix string) []string { 263 keys := make([]string, 0) 264 for k := range d.diff.Attributes { 265 if strings.HasPrefix(k, prefix) { 266 keys = append(keys, k) 267 } 268 } 269 return keys 270 } 271 272 // diffChange helps to implement resourceDiffer and derives its change values 273 // from ResourceDiff's own change data, in addition to existing diff, config, and state. 274 func (d *ResourceDiff) diffChange(key string) (interface{}, interface{}, bool, bool, bool) { 275 old, new, customized := d.getChange(key) 276 277 if !old.Exists { 278 old.Value = nil 279 } 280 if !new.Exists || d.removed(key) { 281 new.Value = nil 282 } 283 284 return old.Value, new.Value, !reflect.DeepEqual(old.Value, new.Value), new.Computed, customized 285 } 286 287 // SetNew is used to set a new diff value for the mentioned key. The value must 288 // be correct for the attribute's schema (mostly relevant for maps, lists, and 289 // sets). The original value from the state is used as the old value. 290 // 291 // This function is only allowed on computed attributes. 292 func (d *ResourceDiff) SetNew(key string, value interface{}) error { 293 if err := d.checkKey(key, "SetNew", false); err != nil { 294 return err 295 } 296 297 return d.setDiff(key, value, false) 298 } 299 300 // SetNewComputed functions like SetNew, except that it blanks out a new value 301 // and marks it as computed. 302 // 303 // This function is only allowed on computed attributes. 304 func (d *ResourceDiff) SetNewComputed(key string) error { 305 if err := d.checkKey(key, "SetNewComputed", false); err != nil { 306 return err 307 } 308 309 return d.setDiff(key, nil, true) 310 } 311 312 // setDiff performs common diff setting behaviour. 313 func (d *ResourceDiff) setDiff(key string, new interface{}, computed bool) error { 314 if err := d.clear(key); err != nil { 315 return err 316 } 317 318 if err := d.newWriter.WriteField(strings.Split(key, "."), new, computed); err != nil { 319 return fmt.Errorf("Cannot set new diff value for key %s: %s", key, err) 320 } 321 322 d.updatedKeys[key] = true 323 324 return nil 325 } 326 327 // ForceNew force-flags ForceNew in the schema for a specific key, and 328 // re-calculates its diff, effectively causing this attribute to force a new 329 // resource. 330 // 331 // Keep in mind that forcing a new resource will force a second run of the 332 // resource's CustomizeDiff function (with a new ResourceDiff) once the current 333 // one has completed. This second run is performed without state. This behavior 334 // will be the same as if a new resource is being created and is performed to 335 // ensure that the diff looks like the diff for a new resource as much as 336 // possible. CustomizeDiff should expect such a scenario and act correctly. 337 // 338 // This function is a no-op/error if there is no diff. 339 // 340 // Note that the change to schema is permanent for the lifecycle of this 341 // specific ResourceDiff instance. 342 func (d *ResourceDiff) ForceNew(key string) error { 343 if !d.HasChange(key) { 344 return fmt.Errorf("ForceNew: No changes for %s", key) 345 } 346 347 keyParts := strings.Split(key, ".") 348 var schema *Schema 349 schemaL := addrToSchema(keyParts, d.schema) 350 if len(schemaL) > 0 { 351 schema = schemaL[len(schemaL)-1] 352 } else { 353 return fmt.Errorf("ForceNew: %s is not a valid key", key) 354 } 355 356 schema.ForceNew = true 357 358 // Flag this for a re-diff. Don't save any values to guarantee that existing 359 // diffs aren't messed with, as this gets messy when dealing with complex 360 // structures, zero values, etc. 361 d.forcedNewKeys[keyParts[0]] = true 362 363 return nil 364 } 365 366 // Get hands off to ResourceData.Get. 367 func (d *ResourceDiff) Get(key string) interface{} { 368 r, _ := d.GetOk(key) 369 return r 370 } 371 372 // GetChange gets the change between the state and diff, checking first to see 373 // if an overridden diff exists. 374 // 375 // This implementation differs from ResourceData's in the way that we first get 376 // results from the exact levels for the new diff, then from state and diff as 377 // per normal. 378 func (d *ResourceDiff) GetChange(key string) (interface{}, interface{}) { 379 old, new, _ := d.getChange(key) 380 return old.Value, new.Value 381 } 382 383 // GetOk functions the same way as ResourceData.GetOk, but it also checks the 384 // new diff levels to provide data consistent with the current state of the 385 // customized diff. 386 func (d *ResourceDiff) GetOk(key string) (interface{}, bool) { 387 r := d.get(strings.Split(key, "."), "newDiff") 388 exists := r.Exists && !r.Computed 389 if exists { 390 // If it exists, we also want to verify it is not the zero-value. 391 value := r.Value 392 zero := r.Schema.Type.Zero() 393 394 if eq, ok := value.(Equal); ok { 395 exists = !eq.Equal(zero) 396 } else { 397 exists = !reflect.DeepEqual(value, zero) 398 } 399 } 400 401 return r.Value, exists 402 } 403 404 // GetOkExists functions the same way as GetOkExists within ResourceData, but 405 // it also checks the new diff levels to provide data consistent with the 406 // current state of the customized diff. 407 // 408 // This is nearly the same function as GetOk, yet it does not check 409 // for the zero value of the attribute's type. This allows for attributes 410 // without a default, to fully check for a literal assignment, regardless 411 // of the zero-value for that type. 412 func (d *ResourceDiff) GetOkExists(key string) (interface{}, bool) { 413 r := d.get(strings.Split(key, "."), "newDiff") 414 exists := r.Exists && !r.Computed 415 return r.Value, exists 416 } 417 418 // NewValueKnown returns true if the new value for the given key is available 419 // as its final value at diff time. If the return value is false, this means 420 // either the value is based of interpolation that was unavailable at diff 421 // time, or that the value was explicitly marked as computed by SetNewComputed. 422 func (d *ResourceDiff) NewValueKnown(key string) bool { 423 r := d.get(strings.Split(key, "."), "newDiff") 424 return !r.Computed 425 } 426 427 // HasChange checks to see if there is a change between state and the diff, or 428 // in the overridden diff. 429 func (d *ResourceDiff) HasChange(key string) bool { 430 old, new := d.GetChange(key) 431 432 // If the type implements the Equal interface, then call that 433 // instead of just doing a reflect.DeepEqual. An example where this is 434 // needed is *Set 435 if eq, ok := old.(Equal); ok { 436 return !eq.Equal(new) 437 } 438 439 return !reflect.DeepEqual(old, new) 440 } 441 442 // Id returns the ID of this resource. 443 // 444 // Note that technically, ID does not change during diffs (it either has 445 // already changed in the refresh, or will change on update), hence we do not 446 // support updating the ID or fetching it from anything else other than state. 447 func (d *ResourceDiff) Id() string { 448 var result string 449 450 if d.state != nil { 451 result = d.state.ID 452 } 453 return result 454 } 455 456 // getChange gets values from two different levels, designed for use in 457 // diffChange, HasChange, and GetChange. 458 // 459 // This implementation differs from ResourceData's in the way that we first get 460 // results from the exact levels for the new diff, then from state and diff as 461 // per normal. 462 func (d *ResourceDiff) getChange(key string) (getResult, getResult, bool) { 463 old := d.get(strings.Split(key, "."), "state") 464 var new getResult 465 for p := range d.updatedKeys { 466 if childAddrOf(key, p) { 467 new = d.getExact(strings.Split(key, "."), "newDiff") 468 return old, new, true 469 } 470 } 471 new = d.get(strings.Split(key, "."), "newDiff") 472 return old, new, false 473 } 474 475 // removed checks to see if the key is present in the existing, pre-customized 476 // diff and if it was marked as NewRemoved. 477 func (d *ResourceDiff) removed(k string) bool { 478 diff, ok := d.diff.Attributes[k] 479 if !ok { 480 return false 481 } 482 return diff.NewRemoved 483 } 484 485 // get performs the appropriate multi-level reader logic for ResourceDiff, 486 // starting at source. Refer to newResourceDiff for the level order. 487 func (d *ResourceDiff) get(addr []string, source string) getResult { 488 result, err := d.multiReader.ReadFieldMerge(addr, source) 489 if err != nil { 490 panic(err) 491 } 492 493 return d.finalizeResult(addr, result) 494 } 495 496 // getExact gets an attribute from the exact level referenced by source. 497 func (d *ResourceDiff) getExact(addr []string, source string) getResult { 498 result, err := d.multiReader.ReadFieldExact(addr, source) 499 if err != nil { 500 panic(err) 501 } 502 503 return d.finalizeResult(addr, result) 504 } 505 506 // finalizeResult does some post-processing of the result produced by get and getExact. 507 func (d *ResourceDiff) finalizeResult(addr []string, result FieldReadResult) getResult { 508 // If the result doesn't exist, then we set the value to the zero value 509 var schema *Schema 510 if schemaL := addrToSchema(addr, d.schema); len(schemaL) > 0 { 511 schema = schemaL[len(schemaL)-1] 512 } 513 514 if result.Value == nil && schema != nil { 515 result.Value = result.ValueOrZero(schema) 516 } 517 518 // Transform the FieldReadResult into a getResult. It might be worth 519 // merging these two structures one day. 520 return getResult{ 521 Value: result.Value, 522 ValueProcessed: result.ValueProcessed, 523 Computed: result.Computed, 524 Exists: result.Exists, 525 Schema: schema, 526 } 527 } 528 529 // childAddrOf does a comparison of two addresses to see if one is the child of 530 // the other. 531 func childAddrOf(child, parent string) bool { 532 cs := strings.Split(child, ".") 533 ps := strings.Split(parent, ".") 534 if len(ps) > len(cs) { 535 return false 536 } 537 return reflect.DeepEqual(ps, cs[:len(ps)]) 538 } 539 540 // checkKey checks the key to make sure it exists and is computed. 541 func (d *ResourceDiff) checkKey(key, caller string, nested bool) error { 542 var schema *Schema 543 if nested { 544 keyParts := strings.Split(key, ".") 545 schemaL := addrToSchema(keyParts, d.schema) 546 if len(schemaL) > 0 { 547 schema = schemaL[len(schemaL)-1] 548 } 549 } else { 550 s, ok := d.schema[key] 551 if ok { 552 schema = s 553 } 554 } 555 if schema == nil { 556 return fmt.Errorf("%s: invalid key: %s", caller, key) 557 } 558 if !schema.Computed { 559 return fmt.Errorf("%s only operates on computed keys - %s is not one", caller, key) 560 } 561 return nil 562 }