github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/legacy/helper/schema/resource_data.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package schema 5 6 import ( 7 "log" 8 "reflect" 9 "strings" 10 "sync" 11 "time" 12 13 "github.com/terramate-io/tf/legacy/terraform" 14 "github.com/zclconf/go-cty/cty" 15 "github.com/zclconf/go-cty/cty/gocty" 16 ) 17 18 // ResourceData is used to query and set the attributes of a resource. 19 // 20 // ResourceData is the primary argument received for CRUD operations on 21 // a resource as well as configuration of a provider. It is a powerful 22 // structure that can be used to not only query data, but check for changes, 23 // define partial state updates, etc. 24 // 25 // The most relevant methods to take a look at are Get, Set, and Partial. 26 type ResourceData struct { 27 // Settable (internally) 28 schema map[string]*Schema 29 config *terraform.ResourceConfig 30 state *terraform.InstanceState 31 diff *terraform.InstanceDiff 32 meta map[string]interface{} 33 timeouts *ResourceTimeout 34 providerMeta cty.Value 35 36 // Don't set 37 multiReader *MultiLevelFieldReader 38 setWriter *MapFieldWriter 39 newState *terraform.InstanceState 40 partial bool 41 partialMap map[string]struct{} 42 once sync.Once 43 isNew bool 44 45 panicOnError bool 46 } 47 48 // getResult is the internal structure that is generated when a Get 49 // is called that contains some extra data that might be used. 50 type getResult struct { 51 Value interface{} 52 ValueProcessed interface{} 53 Computed bool 54 Exists bool 55 Schema *Schema 56 } 57 58 // UnsafeSetFieldRaw allows setting arbitrary values in state to arbitrary 59 // values, bypassing schema. This MUST NOT be used in normal circumstances - 60 // it exists only to support the remote_state data source. 61 // 62 // Deprecated: Fully define schema attributes and use Set() instead. 63 func (d *ResourceData) UnsafeSetFieldRaw(key string, value string) { 64 d.once.Do(d.init) 65 66 d.setWriter.unsafeWriteField(key, value) 67 } 68 69 // Get returns the data for the given key, or nil if the key doesn't exist 70 // in the schema. 71 // 72 // If the key does exist in the schema but doesn't exist in the configuration, 73 // then the default value for that type will be returned. For strings, this is 74 // "", for numbers it is 0, etc. 75 // 76 // If you want to test if something is set at all in the configuration, 77 // use GetOk. 78 func (d *ResourceData) Get(key string) interface{} { 79 v, _ := d.GetOk(key) 80 return v 81 } 82 83 // GetChange returns the old and new value for a given key. 84 // 85 // HasChange should be used to check if a change exists. It is possible 86 // that both the old and new value are the same if the old value was not 87 // set and the new value is. This is common, for example, for boolean 88 // fields which have a zero value of false. 89 func (d *ResourceData) GetChange(key string) (interface{}, interface{}) { 90 o, n := d.getChange(key, getSourceState, getSourceDiff) 91 return o.Value, n.Value 92 } 93 94 // GetOk returns the data for the given key and whether or not the key 95 // has been set to a non-zero value at some point. 96 // 97 // The first result will not necessarilly be nil if the value doesn't exist. 98 // The second result should be checked to determine this information. 99 func (d *ResourceData) GetOk(key string) (interface{}, bool) { 100 r := d.getRaw(key, getSourceSet) 101 exists := r.Exists && !r.Computed 102 if exists { 103 // If it exists, we also want to verify it is not the zero-value. 104 value := r.Value 105 zero := r.Schema.Type.Zero() 106 107 if eq, ok := value.(Equal); ok { 108 exists = !eq.Equal(zero) 109 } else { 110 exists = !reflect.DeepEqual(value, zero) 111 } 112 } 113 114 return r.Value, exists 115 } 116 117 // GetOkExists returns the data for a given key and whether or not the key 118 // has been set to a non-zero value. This is only useful for determining 119 // if boolean attributes have been set, if they are Optional but do not 120 // have a Default value. 121 // 122 // This is nearly the same function as GetOk, yet it does not check 123 // for the zero value of the attribute's type. This allows for attributes 124 // without a default, to fully check for a literal assignment, regardless 125 // of the zero-value for that type. 126 // This should only be used if absolutely required/needed. 127 func (d *ResourceData) GetOkExists(key string) (interface{}, bool) { 128 r := d.getRaw(key, getSourceSet) 129 exists := r.Exists && !r.Computed 130 return r.Value, exists 131 } 132 133 func (d *ResourceData) getRaw(key string, level getSource) getResult { 134 var parts []string 135 if key != "" { 136 parts = strings.Split(key, ".") 137 } 138 139 return d.get(parts, level) 140 } 141 142 // HasChange returns whether or not the given key has been changed. 143 func (d *ResourceData) HasChange(key string) bool { 144 o, n := d.GetChange(key) 145 146 // If the type implements the Equal interface, then call that 147 // instead of just doing a reflect.DeepEqual. An example where this is 148 // needed is *Set 149 if eq, ok := o.(Equal); ok { 150 return !eq.Equal(n) 151 } 152 153 return !reflect.DeepEqual(o, n) 154 } 155 156 // Partial turns partial state mode on/off. 157 // 158 // When partial state mode is enabled, then only key prefixes specified 159 // by SetPartial will be in the final state. This allows providers to return 160 // partial states for partially applied resources (when errors occur). 161 func (d *ResourceData) Partial(on bool) { 162 d.partial = on 163 if on { 164 if d.partialMap == nil { 165 d.partialMap = make(map[string]struct{}) 166 } 167 } else { 168 d.partialMap = nil 169 } 170 } 171 172 // Set sets the value for the given key. 173 // 174 // If the key is invalid or the value is not a correct type, an error 175 // will be returned. 176 func (d *ResourceData) Set(key string, value interface{}) error { 177 d.once.Do(d.init) 178 179 // If the value is a pointer to a non-struct, get its value and 180 // use that. This allows Set to take a pointer to primitives to 181 // simplify the interface. 182 reflectVal := reflect.ValueOf(value) 183 if reflectVal.Kind() == reflect.Ptr { 184 if reflectVal.IsNil() { 185 // If the pointer is nil, then the value is just nil 186 value = nil 187 } else { 188 // Otherwise, we dereference the pointer as long as its not 189 // a pointer to a struct, since struct pointers are allowed. 190 reflectVal = reflect.Indirect(reflectVal) 191 if reflectVal.Kind() != reflect.Struct { 192 value = reflectVal.Interface() 193 } 194 } 195 } 196 197 err := d.setWriter.WriteField(strings.Split(key, "."), value) 198 if err != nil && d.panicOnError { 199 panic(err) 200 } 201 return err 202 } 203 204 // SetPartial adds the key to the final state output while 205 // in partial state mode. The key must be a root key in the schema (i.e. 206 // it cannot be "list.0"). 207 // 208 // If partial state mode is disabled, then this has no effect. Additionally, 209 // whenever partial state mode is toggled, the partial data is cleared. 210 func (d *ResourceData) SetPartial(k string) { 211 if d.partial { 212 d.partialMap[k] = struct{}{} 213 } 214 } 215 216 func (d *ResourceData) MarkNewResource() { 217 d.isNew = true 218 } 219 220 func (d *ResourceData) IsNewResource() bool { 221 return d.isNew 222 } 223 224 // Id returns the ID of the resource. 225 func (d *ResourceData) Id() string { 226 var result string 227 228 if d.state != nil { 229 result = d.state.ID 230 if result == "" { 231 result = d.state.Attributes["id"] 232 } 233 } 234 235 if d.newState != nil { 236 result = d.newState.ID 237 if result == "" { 238 result = d.newState.Attributes["id"] 239 } 240 } 241 242 return result 243 } 244 245 // ConnInfo returns the connection info for this resource. 246 func (d *ResourceData) ConnInfo() map[string]string { 247 if d.newState != nil { 248 return d.newState.Ephemeral.ConnInfo 249 } 250 251 if d.state != nil { 252 return d.state.Ephemeral.ConnInfo 253 } 254 255 return nil 256 } 257 258 // SetId sets the ID of the resource. If the value is blank, then the 259 // resource is destroyed. 260 func (d *ResourceData) SetId(v string) { 261 d.once.Do(d.init) 262 d.newState.ID = v 263 264 // once we transition away from the legacy state types, "id" will no longer 265 // be a special field, and will become a normal attribute. 266 // set the attribute normally 267 d.setWriter.unsafeWriteField("id", v) 268 269 // Make sure the newState is also set, otherwise the old value 270 // may get precedence. 271 if d.newState.Attributes == nil { 272 d.newState.Attributes = map[string]string{} 273 } 274 d.newState.Attributes["id"] = v 275 } 276 277 // SetConnInfo sets the connection info for a resource. 278 func (d *ResourceData) SetConnInfo(v map[string]string) { 279 d.once.Do(d.init) 280 d.newState.Ephemeral.ConnInfo = v 281 } 282 283 // SetType sets the ephemeral type for the data. This is only required 284 // for importing. 285 func (d *ResourceData) SetType(t string) { 286 d.once.Do(d.init) 287 d.newState.Ephemeral.Type = t 288 } 289 290 // State returns the new InstanceState after the diff and any Set 291 // calls. 292 func (d *ResourceData) State() *terraform.InstanceState { 293 var result terraform.InstanceState 294 result.ID = d.Id() 295 result.Meta = d.meta 296 297 // If we have no ID, then this resource doesn't exist and we just 298 // return nil. 299 if result.ID == "" { 300 return nil 301 } 302 303 if d.timeouts != nil { 304 if err := d.timeouts.StateEncode(&result); err != nil { 305 log.Printf("[ERR] Error encoding Timeout meta to Instance State: %s", err) 306 } 307 } 308 309 // Look for a magic key in the schema that determines we skip the 310 // integrity check of fields existing in the schema, allowing dynamic 311 // keys to be created. 312 hasDynamicAttributes := false 313 for k, _ := range d.schema { 314 if k == "__has_dynamic_attributes" { 315 hasDynamicAttributes = true 316 log.Printf("[INFO] Resource %s has dynamic attributes", result.ID) 317 } 318 } 319 320 // In order to build the final state attributes, we read the full 321 // attribute set as a map[string]interface{}, write it to a MapFieldWriter, 322 // and then use that map. 323 rawMap := make(map[string]interface{}) 324 for k := range d.schema { 325 source := getSourceSet 326 if d.partial { 327 source = getSourceState 328 if _, ok := d.partialMap[k]; ok { 329 source = getSourceSet 330 } 331 } 332 333 raw := d.get([]string{k}, source) 334 if raw.Exists && !raw.Computed { 335 rawMap[k] = raw.Value 336 if raw.ValueProcessed != nil { 337 rawMap[k] = raw.ValueProcessed 338 } 339 } 340 } 341 342 mapW := &MapFieldWriter{Schema: d.schema} 343 if err := mapW.WriteField(nil, rawMap); err != nil { 344 log.Printf("[ERR] Error writing fields: %s", err) 345 return nil 346 } 347 348 result.Attributes = mapW.Map() 349 350 if hasDynamicAttributes { 351 // If we have dynamic attributes, just copy the attributes map 352 // one for one into the result attributes. 353 for k, v := range d.setWriter.Map() { 354 // Don't clobber schema values. This limits usage of dynamic 355 // attributes to names which _do not_ conflict with schema 356 // keys! 357 if _, ok := result.Attributes[k]; !ok { 358 result.Attributes[k] = v 359 } 360 } 361 } 362 363 if d.newState != nil { 364 result.Ephemeral = d.newState.Ephemeral 365 } 366 367 // TODO: This is hacky and we can remove this when we have a proper 368 // state writer. We should instead have a proper StateFieldWriter 369 // and use that. 370 for k, schema := range d.schema { 371 if schema.Type != TypeMap { 372 continue 373 } 374 375 if result.Attributes[k] == "" { 376 delete(result.Attributes, k) 377 } 378 } 379 380 if v := d.Id(); v != "" { 381 result.Attributes["id"] = d.Id() 382 } 383 384 if d.state != nil { 385 result.Tainted = d.state.Tainted 386 } 387 388 return &result 389 } 390 391 // Timeout returns the data for the given timeout key 392 // Returns a duration of 20 minutes for any key not found, or not found and no default. 393 func (d *ResourceData) Timeout(key string) time.Duration { 394 key = strings.ToLower(key) 395 396 // System default of 20 minutes 397 defaultTimeout := 20 * time.Minute 398 399 if d.timeouts == nil { 400 return defaultTimeout 401 } 402 403 var timeout *time.Duration 404 switch key { 405 case TimeoutCreate: 406 timeout = d.timeouts.Create 407 case TimeoutRead: 408 timeout = d.timeouts.Read 409 case TimeoutUpdate: 410 timeout = d.timeouts.Update 411 case TimeoutDelete: 412 timeout = d.timeouts.Delete 413 } 414 415 if timeout != nil { 416 return *timeout 417 } 418 419 if d.timeouts.Default != nil { 420 return *d.timeouts.Default 421 } 422 423 return defaultTimeout 424 } 425 426 func (d *ResourceData) init() { 427 // Initialize the field that will store our new state 428 var copyState terraform.InstanceState 429 if d.state != nil { 430 copyState = *d.state.DeepCopy() 431 } 432 d.newState = ©State 433 434 // Initialize the map for storing set data 435 d.setWriter = &MapFieldWriter{Schema: d.schema} 436 437 // Initialize the reader for getting data from the 438 // underlying sources (config, diff, etc.) 439 readers := make(map[string]FieldReader) 440 var stateAttributes map[string]string 441 if d.state != nil { 442 stateAttributes = d.state.Attributes 443 readers["state"] = &MapFieldReader{ 444 Schema: d.schema, 445 Map: BasicMapReader(stateAttributes), 446 } 447 } 448 if d.config != nil { 449 readers["config"] = &ConfigFieldReader{ 450 Schema: d.schema, 451 Config: d.config, 452 } 453 } 454 if d.diff != nil { 455 readers["diff"] = &DiffFieldReader{ 456 Schema: d.schema, 457 Diff: d.diff, 458 Source: &MultiLevelFieldReader{ 459 Levels: []string{"state", "config"}, 460 Readers: readers, 461 }, 462 } 463 } 464 readers["set"] = &MapFieldReader{ 465 Schema: d.schema, 466 Map: BasicMapReader(d.setWriter.Map()), 467 } 468 d.multiReader = &MultiLevelFieldReader{ 469 Levels: []string{ 470 "state", 471 "config", 472 "diff", 473 "set", 474 }, 475 476 Readers: readers, 477 } 478 } 479 480 func (d *ResourceData) diffChange( 481 k string) (interface{}, interface{}, bool, bool, bool) { 482 // Get the change between the state and the config. 483 o, n := d.getChange(k, getSourceState, getSourceConfig|getSourceExact) 484 if !o.Exists { 485 o.Value = nil 486 } 487 if !n.Exists { 488 n.Value = nil 489 } 490 491 // Return the old, new, and whether there is a change 492 return o.Value, n.Value, !reflect.DeepEqual(o.Value, n.Value), n.Computed, false 493 } 494 495 func (d *ResourceData) getChange( 496 k string, 497 oldLevel getSource, 498 newLevel getSource) (getResult, getResult) { 499 var parts, parts2 []string 500 if k != "" { 501 parts = strings.Split(k, ".") 502 parts2 = strings.Split(k, ".") 503 } 504 505 o := d.get(parts, oldLevel) 506 n := d.get(parts2, newLevel) 507 return o, n 508 } 509 510 func (d *ResourceData) get(addr []string, source getSource) getResult { 511 d.once.Do(d.init) 512 513 level := "set" 514 flags := source & ^getSourceLevelMask 515 exact := flags&getSourceExact != 0 516 source = source & getSourceLevelMask 517 if source >= getSourceSet { 518 level = "set" 519 } else if source >= getSourceDiff { 520 level = "diff" 521 } else if source >= getSourceConfig { 522 level = "config" 523 } else { 524 level = "state" 525 } 526 527 var result FieldReadResult 528 var err error 529 if exact { 530 result, err = d.multiReader.ReadFieldExact(addr, level) 531 } else { 532 result, err = d.multiReader.ReadFieldMerge(addr, level) 533 } 534 if err != nil { 535 panic(err) 536 } 537 538 // If the result doesn't exist, then we set the value to the zero value 539 var schema *Schema 540 if schemaL := addrToSchema(addr, d.schema); len(schemaL) > 0 { 541 schema = schemaL[len(schemaL)-1] 542 } 543 544 if result.Value == nil && schema != nil { 545 result.Value = result.ValueOrZero(schema) 546 } 547 548 // Transform the FieldReadResult into a getResult. It might be worth 549 // merging these two structures one day. 550 return getResult{ 551 Value: result.Value, 552 ValueProcessed: result.ValueProcessed, 553 Computed: result.Computed, 554 Exists: result.Exists, 555 Schema: schema, 556 } 557 } 558 559 func (d *ResourceData) GetProviderMeta(dst interface{}) error { 560 if d.providerMeta.IsNull() { 561 return nil 562 } 563 return gocty.FromCtyValue(d.providerMeta, &dst) 564 }