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