github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/legacy/helper/schema/resource.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 "log" 10 "strconv" 11 12 "github.com/terramate-io/tf/legacy/terraform" 13 "github.com/zclconf/go-cty/cty" 14 ) 15 16 var ReservedDataSourceFields = []string{ 17 "connection", 18 "count", 19 "depends_on", 20 "lifecycle", 21 "provider", 22 "provisioner", 23 } 24 25 var ReservedResourceFields = []string{ 26 "connection", 27 "count", 28 "depends_on", 29 "id", 30 "lifecycle", 31 "provider", 32 "provisioner", 33 } 34 35 // Resource represents a thing in Terraform that has a set of configurable 36 // attributes and a lifecycle (create, read, update, delete). 37 // 38 // The Resource schema is an abstraction that allows provider writers to 39 // worry only about CRUD operations while off-loading validation, diff 40 // generation, etc. to this higher level library. 41 // 42 // In spite of the name, this struct is not used only for terraform resources, 43 // but also for data sources. In the case of data sources, the Create, 44 // Update and Delete functions must not be provided. 45 type Resource struct { 46 // Schema is the schema for the configuration of this resource. 47 // 48 // The keys of this map are the configuration keys, and the values 49 // describe the schema of the configuration value. 50 // 51 // The schema is used to represent both configurable data as well 52 // as data that might be computed in the process of creating this 53 // resource. 54 Schema map[string]*Schema 55 56 // SchemaVersion is the version number for this resource's Schema 57 // definition. The current SchemaVersion stored in the state for each 58 // resource. Provider authors can increment this version number 59 // when Schema semantics change. If the State's SchemaVersion is less than 60 // the current SchemaVersion, the InstanceState is yielded to the 61 // MigrateState callback, where the provider can make whatever changes it 62 // needs to update the state to be compatible to the latest version of the 63 // Schema. 64 // 65 // When unset, SchemaVersion defaults to 0, so provider authors can start 66 // their Versioning at any integer >= 1 67 SchemaVersion int 68 69 // MigrateState is deprecated and any new changes to a resource's schema 70 // should be handled by StateUpgraders. Existing MigrateState implementations 71 // should remain for compatibility with existing state. MigrateState will 72 // still be called if the stored SchemaVersion is less than the 73 // first version of the StateUpgraders. 74 // 75 // MigrateState is responsible for updating an InstanceState with an old 76 // version to the format expected by the current version of the Schema. 77 // 78 // It is called during Refresh if the State's stored SchemaVersion is less 79 // than the current SchemaVersion of the Resource. 80 // 81 // The function is yielded the state's stored SchemaVersion and a pointer to 82 // the InstanceState that needs updating, as well as the configured 83 // provider's configured meta interface{}, in case the migration process 84 // needs to make any remote API calls. 85 MigrateState StateMigrateFunc 86 87 // StateUpgraders contains the functions responsible for upgrading an 88 // existing state with an old schema version to a newer schema. It is 89 // called specifically by Terraform when the stored schema version is less 90 // than the current SchemaVersion of the Resource. 91 // 92 // StateUpgraders map specific schema versions to a StateUpgrader 93 // function. The registered versions are expected to be ordered, 94 // consecutive values. The initial value may be greater than 0 to account 95 // for legacy schemas that weren't recorded and can be handled by 96 // MigrateState. 97 StateUpgraders []StateUpgrader 98 99 // The functions below are the CRUD operations for this resource. 100 // 101 // The only optional operation is Update. If Update is not implemented, 102 // then updates will not be supported for this resource. 103 // 104 // The ResourceData parameter in the functions below are used to 105 // query configuration and changes for the resource as well as to set 106 // the ID, computed data, etc. 107 // 108 // The interface{} parameter is the result of the ConfigureFunc in 109 // the provider for this resource. If the provider does not define 110 // a ConfigureFunc, this will be nil. This parameter should be used 111 // to store API clients, configuration structures, etc. 112 // 113 // If any errors occur during each of the operation, an error should be 114 // returned. If a resource was partially updated, be careful to enable 115 // partial state mode for ResourceData and use it accordingly. 116 // 117 // Exists is a function that is called to check if a resource still 118 // exists. If this returns false, then this will affect the diff 119 // accordingly. If this function isn't set, it will not be called. You 120 // can also signal existence in the Read method by calling d.SetId("") 121 // if the Resource is no longer present and should be removed from state. 122 // The *ResourceData passed to Exists should _not_ be modified. 123 Create CreateFunc 124 Read ReadFunc 125 Update UpdateFunc 126 Delete DeleteFunc 127 Exists ExistsFunc 128 129 // CustomizeDiff is a custom function for working with the diff that 130 // Terraform has created for this resource - it can be used to customize the 131 // diff that has been created, diff values not controlled by configuration, 132 // or even veto the diff altogether and abort the plan. It is passed a 133 // *ResourceDiff, a structure similar to ResourceData but lacking most write 134 // functions like Set, while introducing new functions that work with the 135 // diff such as SetNew, SetNewComputed, and ForceNew. 136 // 137 // The phases Terraform runs this in, and the state available via functions 138 // like Get and GetChange, are as follows: 139 // 140 // * New resource: One run with no state 141 // * Existing resource: One run with state 142 // * Existing resource, forced new: One run with state (before ForceNew), 143 // then one run without state (as if new resource) 144 // * Tainted resource: No runs (custom diff logic is skipped) 145 // * Destroy: No runs (standard diff logic is skipped on destroy diffs) 146 // 147 // This function needs to be resilient to support all scenarios. 148 // 149 // If this function needs to access external API resources, remember to flag 150 // the RequiresRefresh attribute mentioned below to ensure that 151 // -refresh=false is blocked when running plan or apply, as this means that 152 // this resource requires refresh-like behaviour to work effectively. 153 // 154 // For the most part, only computed fields can be customized by this 155 // function. 156 // 157 // This function is only allowed on regular resources (not data sources). 158 CustomizeDiff CustomizeDiffFunc 159 160 // Importer is the ResourceImporter implementation for this resource. 161 // If this is nil, then this resource does not support importing. If 162 // this is non-nil, then it supports importing and ResourceImporter 163 // must be validated. The validity of ResourceImporter is verified 164 // by InternalValidate on Resource. 165 Importer *ResourceImporter 166 167 // If non-empty, this string is emitted as a warning during Validate. 168 DeprecationMessage string 169 170 // Timeouts allow users to specify specific time durations in which an 171 // operation should time out, to allow them to extend an action to suit their 172 // usage. For example, a user may specify a large Creation timeout for their 173 // AWS RDS Instance due to it's size, or restoring from a snapshot. 174 // Resource implementors must enable Timeout support by adding the allowed 175 // actions (Create, Read, Update, Delete, Default) to the Resource struct, and 176 // accessing them in the matching methods. 177 Timeouts *ResourceTimeout 178 } 179 180 // ShimInstanceStateFromValue converts a cty.Value to a 181 // terraform.InstanceState. 182 func (r *Resource) ShimInstanceStateFromValue(state cty.Value) (*terraform.InstanceState, error) { 183 // Get the raw shimmed value. While this is correct, the set hashes don't 184 // match those from the Schema. 185 s := terraform.NewInstanceStateShimmedFromValue(state, r.SchemaVersion) 186 187 // We now rebuild the state through the ResourceData, so that the set indexes 188 // match what helper/schema expects. 189 data, err := schemaMap(r.Schema).Data(s, nil) 190 if err != nil { 191 return nil, err 192 } 193 194 s = data.State() 195 if s == nil { 196 s = &terraform.InstanceState{} 197 } 198 return s, nil 199 } 200 201 // See Resource documentation. 202 type CreateFunc func(*ResourceData, interface{}) error 203 204 // See Resource documentation. 205 type ReadFunc func(*ResourceData, interface{}) error 206 207 // See Resource documentation. 208 type UpdateFunc func(*ResourceData, interface{}) error 209 210 // See Resource documentation. 211 type DeleteFunc func(*ResourceData, interface{}) error 212 213 // See Resource documentation. 214 type ExistsFunc func(*ResourceData, interface{}) (bool, error) 215 216 // See Resource documentation. 217 type StateMigrateFunc func( 218 int, *terraform.InstanceState, interface{}) (*terraform.InstanceState, error) 219 220 type StateUpgrader struct { 221 // Version is the version schema that this Upgrader will handle, converting 222 // it to Version+1. 223 Version int 224 225 // Type describes the schema that this function can upgrade. Type is 226 // required to decode the schema if the state was stored in a legacy 227 // flatmap format. 228 Type cty.Type 229 230 // Upgrade takes the JSON encoded state and the provider meta value, and 231 // upgrades the state one single schema version. The provided state is 232 // deocded into the default json types using a map[string]interface{}. It 233 // is up to the StateUpgradeFunc to ensure that the returned value can be 234 // encoded using the new schema. 235 Upgrade StateUpgradeFunc 236 } 237 238 // See StateUpgrader 239 type StateUpgradeFunc func(rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) 240 241 // See Resource documentation. 242 type CustomizeDiffFunc func(*ResourceDiff, interface{}) error 243 244 // Apply creates, updates, and/or deletes a resource. 245 func (r *Resource) Apply( 246 s *terraform.InstanceState, 247 d *terraform.InstanceDiff, 248 meta interface{}) (*terraform.InstanceState, error) { 249 data, err := schemaMap(r.Schema).Data(s, d) 250 if err != nil { 251 return s, err 252 } 253 if s != nil && data != nil { 254 data.providerMeta = s.ProviderMeta 255 } 256 257 // Instance Diff shoould have the timeout info, need to copy it over to the 258 // ResourceData meta 259 rt := ResourceTimeout{} 260 if _, ok := d.Meta[TimeoutKey]; ok { 261 if err := rt.DiffDecode(d); err != nil { 262 log.Printf("[ERR] Error decoding ResourceTimeout: %s", err) 263 } 264 } else if s != nil { 265 if _, ok := s.Meta[TimeoutKey]; ok { 266 if err := rt.StateDecode(s); err != nil { 267 log.Printf("[ERR] Error decoding ResourceTimeout: %s", err) 268 } 269 } 270 } else { 271 log.Printf("[DEBUG] No meta timeoutkey found in Apply()") 272 } 273 data.timeouts = &rt 274 275 if s == nil { 276 // The Terraform API dictates that this should never happen, but 277 // it doesn't hurt to be safe in this case. 278 s = new(terraform.InstanceState) 279 } 280 281 if d.Destroy || d.RequiresNew() { 282 if s.ID != "" { 283 // Destroy the resource since it is created 284 if err := r.Delete(data, meta); err != nil { 285 return r.recordCurrentSchemaVersion(data.State()), err 286 } 287 288 // Make sure the ID is gone. 289 data.SetId("") 290 } 291 292 // If we're only destroying, and not creating, then return 293 // now since we're done! 294 if !d.RequiresNew() { 295 return nil, nil 296 } 297 298 // Reset the data to be stateless since we just destroyed 299 data, err = schemaMap(r.Schema).Data(nil, d) 300 // data was reset, need to re-apply the parsed timeouts 301 data.timeouts = &rt 302 if err != nil { 303 return nil, err 304 } 305 } 306 307 err = nil 308 if data.Id() == "" { 309 // We're creating, it is a new resource. 310 data.MarkNewResource() 311 err = r.Create(data, meta) 312 } else { 313 if r.Update == nil { 314 return s, fmt.Errorf("doesn't support update") 315 } 316 317 err = r.Update(data, meta) 318 } 319 320 return r.recordCurrentSchemaVersion(data.State()), err 321 } 322 323 // Diff returns a diff of this resource. 324 func (r *Resource) Diff( 325 s *terraform.InstanceState, 326 c *terraform.ResourceConfig, 327 meta interface{}) (*terraform.InstanceDiff, error) { 328 329 t := &ResourceTimeout{} 330 err := t.ConfigDecode(r, c) 331 332 if err != nil { 333 return nil, fmt.Errorf("[ERR] Error decoding timeout: %s", err) 334 } 335 336 instanceDiff, err := schemaMap(r.Schema).Diff(s, c, r.CustomizeDiff, meta, true) 337 if err != nil { 338 return instanceDiff, err 339 } 340 341 if instanceDiff != nil { 342 if err := t.DiffEncode(instanceDiff); err != nil { 343 log.Printf("[ERR] Error encoding timeout to instance diff: %s", err) 344 } 345 } else { 346 log.Printf("[DEBUG] Instance Diff is nil in Diff()") 347 } 348 349 return instanceDiff, err 350 } 351 352 func (r *Resource) simpleDiff( 353 s *terraform.InstanceState, 354 c *terraform.ResourceConfig, 355 meta interface{}) (*terraform.InstanceDiff, error) { 356 357 instanceDiff, err := schemaMap(r.Schema).Diff(s, c, r.CustomizeDiff, meta, false) 358 if err != nil { 359 return instanceDiff, err 360 } 361 362 if instanceDiff == nil { 363 instanceDiff = terraform.NewInstanceDiff() 364 } 365 366 // Make sure the old value is set in each of the instance diffs. 367 // This was done by the RequiresNew logic in the full legacy Diff. 368 for k, attr := range instanceDiff.Attributes { 369 if attr == nil { 370 continue 371 } 372 if s != nil { 373 attr.Old = s.Attributes[k] 374 } 375 } 376 377 return instanceDiff, nil 378 } 379 380 // Validate validates the resource configuration against the schema. 381 func (r *Resource) Validate(c *terraform.ResourceConfig) ([]string, []error) { 382 warns, errs := schemaMap(r.Schema).Validate(c) 383 384 if r.DeprecationMessage != "" { 385 warns = append(warns, r.DeprecationMessage) 386 } 387 388 return warns, errs 389 } 390 391 // ReadDataApply loads the data for a data source, given a diff that 392 // describes the configuration arguments and desired computed attributes. 393 func (r *Resource) ReadDataApply( 394 d *terraform.InstanceDiff, 395 meta interface{}, 396 ) (*terraform.InstanceState, error) { 397 // Data sources are always built completely from scratch 398 // on each read, so the source state is always nil. 399 data, err := schemaMap(r.Schema).Data(nil, d) 400 if err != nil { 401 return nil, err 402 } 403 404 err = r.Read(data, meta) 405 state := data.State() 406 if state != nil && state.ID == "" { 407 // Data sources can set an ID if they want, but they aren't 408 // required to; we'll provide a placeholder if they don't, 409 // to preserve the invariant that all resources have non-empty 410 // ids. 411 state.ID = "-" 412 } 413 414 return r.recordCurrentSchemaVersion(state), err 415 } 416 417 // RefreshWithoutUpgrade reads the instance state, but does not call 418 // MigrateState or the StateUpgraders, since those are now invoked in a 419 // separate API call. 420 // RefreshWithoutUpgrade is part of the new plugin shims. 421 func (r *Resource) RefreshWithoutUpgrade( 422 s *terraform.InstanceState, 423 meta interface{}) (*terraform.InstanceState, error) { 424 // If the ID is already somehow blank, it doesn't exist 425 if s.ID == "" { 426 return nil, nil 427 } 428 429 rt := ResourceTimeout{} 430 if _, ok := s.Meta[TimeoutKey]; ok { 431 if err := rt.StateDecode(s); err != nil { 432 log.Printf("[ERR] Error decoding ResourceTimeout: %s", err) 433 } 434 } 435 436 if r.Exists != nil { 437 // Make a copy of data so that if it is modified it doesn't 438 // affect our Read later. 439 data, err := schemaMap(r.Schema).Data(s, nil) 440 data.timeouts = &rt 441 442 if err != nil { 443 return s, err 444 } 445 446 if s != nil { 447 data.providerMeta = s.ProviderMeta 448 } 449 450 exists, err := r.Exists(data, meta) 451 if err != nil { 452 return s, err 453 } 454 if !exists { 455 return nil, nil 456 } 457 } 458 459 data, err := schemaMap(r.Schema).Data(s, nil) 460 data.timeouts = &rt 461 if err != nil { 462 return s, err 463 } 464 465 if s != nil { 466 data.providerMeta = s.ProviderMeta 467 } 468 469 err = r.Read(data, meta) 470 state := data.State() 471 if state != nil && state.ID == "" { 472 state = nil 473 } 474 475 return r.recordCurrentSchemaVersion(state), err 476 } 477 478 // Refresh refreshes the state of the resource. 479 func (r *Resource) Refresh( 480 s *terraform.InstanceState, 481 meta interface{}) (*terraform.InstanceState, error) { 482 // If the ID is already somehow blank, it doesn't exist 483 if s.ID == "" { 484 return nil, nil 485 } 486 487 rt := ResourceTimeout{} 488 if _, ok := s.Meta[TimeoutKey]; ok { 489 if err := rt.StateDecode(s); err != nil { 490 log.Printf("[ERR] Error decoding ResourceTimeout: %s", err) 491 } 492 } 493 494 if r.Exists != nil { 495 // Make a copy of data so that if it is modified it doesn't 496 // affect our Read later. 497 data, err := schemaMap(r.Schema).Data(s, nil) 498 data.timeouts = &rt 499 500 if err != nil { 501 return s, err 502 } 503 504 exists, err := r.Exists(data, meta) 505 if err != nil { 506 return s, err 507 } 508 if !exists { 509 return nil, nil 510 } 511 } 512 513 // there may be new StateUpgraders that need to be run 514 s, err := r.upgradeState(s, meta) 515 if err != nil { 516 return s, err 517 } 518 519 data, err := schemaMap(r.Schema).Data(s, nil) 520 data.timeouts = &rt 521 if err != nil { 522 return s, err 523 } 524 525 err = r.Read(data, meta) 526 state := data.State() 527 if state != nil && state.ID == "" { 528 state = nil 529 } 530 531 return r.recordCurrentSchemaVersion(state), err 532 } 533 534 func (r *Resource) upgradeState(s *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { 535 var err error 536 537 needsMigration, stateSchemaVersion := r.checkSchemaVersion(s) 538 migrate := needsMigration && r.MigrateState != nil 539 540 if migrate { 541 s, err = r.MigrateState(stateSchemaVersion, s, meta) 542 if err != nil { 543 return s, err 544 } 545 } 546 547 if len(r.StateUpgraders) == 0 { 548 return s, nil 549 } 550 551 // If we ran MigrateState, then the stateSchemaVersion value is no longer 552 // correct. We can expect the first upgrade function to be the correct 553 // schema type version. 554 if migrate { 555 stateSchemaVersion = r.StateUpgraders[0].Version 556 } 557 558 schemaType := r.CoreConfigSchema().ImpliedType() 559 // find the expected type to convert the state 560 for _, upgrader := range r.StateUpgraders { 561 if stateSchemaVersion == upgrader.Version { 562 schemaType = upgrader.Type 563 } 564 } 565 566 // StateUpgraders only operate on the new JSON format state, so the state 567 // need to be converted. 568 stateVal, err := StateValueFromInstanceState(s, schemaType) 569 if err != nil { 570 return nil, err 571 } 572 573 jsonState, err := StateValueToJSONMap(stateVal, schemaType) 574 if err != nil { 575 return nil, err 576 } 577 578 for _, upgrader := range r.StateUpgraders { 579 if stateSchemaVersion != upgrader.Version { 580 continue 581 } 582 583 jsonState, err = upgrader.Upgrade(jsonState, meta) 584 if err != nil { 585 return nil, err 586 } 587 stateSchemaVersion++ 588 } 589 590 // now we need to re-flatmap the new state 591 stateVal, err = JSONMapToStateValue(jsonState, r.CoreConfigSchema()) 592 if err != nil { 593 return nil, err 594 } 595 596 return r.ShimInstanceStateFromValue(stateVal) 597 } 598 599 // InternalValidate should be called to validate the structure 600 // of the resource. 601 // 602 // This should be called in a unit test for any resource to verify 603 // before release that a resource is properly configured for use with 604 // this library. 605 // 606 // Provider.InternalValidate() will automatically call this for all of 607 // the resources it manages, so you don't need to call this manually if it 608 // is part of a Provider. 609 func (r *Resource) InternalValidate(topSchemaMap schemaMap, writable bool) error { 610 if r == nil { 611 return errors.New("resource is nil") 612 } 613 614 if !writable { 615 if r.Create != nil || r.Update != nil || r.Delete != nil { 616 return fmt.Errorf("must not implement Create, Update or Delete") 617 } 618 619 // CustomizeDiff cannot be defined for read-only resources 620 if r.CustomizeDiff != nil { 621 return fmt.Errorf("cannot implement CustomizeDiff") 622 } 623 } 624 625 tsm := topSchemaMap 626 627 if r.isTopLevel() && writable { 628 // All non-Computed attributes must be ForceNew if Update is not defined 629 if r.Update == nil { 630 nonForceNewAttrs := make([]string, 0) 631 for k, v := range r.Schema { 632 if !v.ForceNew && !v.Computed { 633 nonForceNewAttrs = append(nonForceNewAttrs, k) 634 } 635 } 636 if len(nonForceNewAttrs) > 0 { 637 return fmt.Errorf( 638 "No Update defined, must set ForceNew on: %#v", nonForceNewAttrs) 639 } 640 } else { 641 nonUpdateableAttrs := make([]string, 0) 642 for k, v := range r.Schema { 643 if v.ForceNew || v.Computed && !v.Optional { 644 nonUpdateableAttrs = append(nonUpdateableAttrs, k) 645 } 646 } 647 updateableAttrs := len(r.Schema) - len(nonUpdateableAttrs) 648 if updateableAttrs == 0 { 649 return fmt.Errorf( 650 "All fields are ForceNew or Computed w/out Optional, Update is superfluous") 651 } 652 } 653 654 tsm = schemaMap(r.Schema) 655 656 // Destroy, and Read are required 657 if r.Read == nil { 658 return fmt.Errorf("Read must be implemented") 659 } 660 if r.Delete == nil { 661 return fmt.Errorf("Delete must be implemented") 662 } 663 664 // If we have an importer, we need to verify the importer. 665 if r.Importer != nil { 666 if err := r.Importer.InternalValidate(); err != nil { 667 return err 668 } 669 } 670 671 for k, f := range tsm { 672 if isReservedResourceFieldName(k, f) { 673 return fmt.Errorf("%s is a reserved field name", k) 674 } 675 } 676 } 677 678 lastVersion := -1 679 for _, u := range r.StateUpgraders { 680 if lastVersion >= 0 && u.Version-lastVersion > 1 { 681 return fmt.Errorf("missing schema version between %d and %d", lastVersion, u.Version) 682 } 683 684 if u.Version >= r.SchemaVersion { 685 return fmt.Errorf("StateUpgrader version %d is >= current version %d", u.Version, r.SchemaVersion) 686 } 687 688 if !u.Type.IsObjectType() { 689 return fmt.Errorf("StateUpgrader %d type is not cty.Object", u.Version) 690 } 691 692 if u.Upgrade == nil { 693 return fmt.Errorf("StateUpgrader %d missing StateUpgradeFunc", u.Version) 694 } 695 696 lastVersion = u.Version 697 } 698 699 if lastVersion >= 0 && lastVersion != r.SchemaVersion-1 { 700 return fmt.Errorf("missing StateUpgrader between %d and %d", lastVersion, r.SchemaVersion) 701 } 702 703 // Data source 704 if r.isTopLevel() && !writable { 705 tsm = schemaMap(r.Schema) 706 for k, _ := range tsm { 707 if isReservedDataSourceFieldName(k) { 708 return fmt.Errorf("%s is a reserved field name", k) 709 } 710 } 711 } 712 713 return schemaMap(r.Schema).InternalValidate(tsm) 714 } 715 716 func isReservedDataSourceFieldName(name string) bool { 717 for _, reservedName := range ReservedDataSourceFields { 718 if name == reservedName { 719 return true 720 } 721 } 722 return false 723 } 724 725 func isReservedResourceFieldName(name string, s *Schema) bool { 726 // Allow phasing out "id" 727 // See https://github.com/terraform-providers/terraform-provider-aws/pull/1626#issuecomment-328881415 728 if name == "id" && (s.Deprecated != "" || s.Removed != "") { 729 return false 730 } 731 732 for _, reservedName := range ReservedResourceFields { 733 if name == reservedName { 734 return true 735 } 736 } 737 return false 738 } 739 740 // Data returns a ResourceData struct for this Resource. Each return value 741 // is a separate copy and can be safely modified differently. 742 // 743 // The data returned from this function has no actual affect on the Resource 744 // itself (including the state given to this function). 745 // 746 // This function is useful for unit tests and ResourceImporter functions. 747 func (r *Resource) Data(s *terraform.InstanceState) *ResourceData { 748 result, err := schemaMap(r.Schema).Data(s, nil) 749 if err != nil { 750 // At the time of writing, this isn't possible (Data never returns 751 // non-nil errors). We panic to find this in the future if we have to. 752 // I don't see a reason for Data to ever return an error. 753 panic(err) 754 } 755 756 // load the Resource timeouts 757 result.timeouts = r.Timeouts 758 if result.timeouts == nil { 759 result.timeouts = &ResourceTimeout{} 760 } 761 762 // Set the schema version to latest by default 763 result.meta = map[string]interface{}{ 764 "schema_version": strconv.Itoa(r.SchemaVersion), 765 } 766 767 return result 768 } 769 770 // TestResourceData Yields a ResourceData filled with this resource's schema for use in unit testing 771 // 772 // TODO: May be able to be removed with the above ResourceData function. 773 func (r *Resource) TestResourceData() *ResourceData { 774 return &ResourceData{ 775 schema: r.Schema, 776 } 777 } 778 779 // SchemasForFlatmapPath tries its best to find a sequence of schemas that 780 // the given dot-delimited attribute path traverses through in the schema 781 // of the receiving Resource. 782 func (r *Resource) SchemasForFlatmapPath(path string) []*Schema { 783 return SchemasForFlatmapPath(path, r.Schema) 784 } 785 786 // Returns true if the resource is "top level" i.e. not a sub-resource. 787 func (r *Resource) isTopLevel() bool { 788 // TODO: This is a heuristic; replace with a definitive attribute? 789 return (r.Create != nil || r.Read != nil) 790 } 791 792 // Determines if a given InstanceState needs to be migrated by checking the 793 // stored version number with the current SchemaVersion 794 func (r *Resource) checkSchemaVersion(is *terraform.InstanceState) (bool, int) { 795 // Get the raw interface{} value for the schema version. If it doesn't 796 // exist or is nil then set it to zero. 797 raw := is.Meta["schema_version"] 798 if raw == nil { 799 raw = "0" 800 } 801 802 // Try to convert it to a string. If it isn't a string then we pretend 803 // that it isn't set at all. It should never not be a string unless it 804 // was manually tampered with. 805 rawString, ok := raw.(string) 806 if !ok { 807 rawString = "0" 808 } 809 810 stateSchemaVersion, _ := strconv.Atoi(rawString) 811 812 // Don't run MigrateState if the version is handled by a StateUpgrader, 813 // since StateMigrateFuncs are not required to handle unknown versions 814 maxVersion := r.SchemaVersion 815 if len(r.StateUpgraders) > 0 { 816 maxVersion = r.StateUpgraders[0].Version 817 } 818 819 return stateSchemaVersion < maxVersion, stateSchemaVersion 820 } 821 822 func (r *Resource) recordCurrentSchemaVersion( 823 state *terraform.InstanceState) *terraform.InstanceState { 824 if state != nil && r.SchemaVersion > 0 { 825 if state.Meta == nil { 826 state.Meta = make(map[string]interface{}) 827 } 828 state.Meta["schema_version"] = strconv.Itoa(r.SchemaVersion) 829 } 830 return state 831 } 832 833 // Noop is a convenience implementation of resource function which takes 834 // no action and returns no error. 835 func Noop(*ResourceData, interface{}) error { 836 return nil 837 } 838 839 // RemoveFromState is a convenience implementation of a resource function 840 // which sets the resource ID to empty string (to remove it from state) 841 // and returns no error. 842 func RemoveFromState(d *ResourceData, _ interface{}) error { 843 d.SetId("") 844 return nil 845 }