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