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  }