github.com/ben-turner/terraform@v0.11.8-0.20180503104400-0cc9e050ecd4/helper/schema/resource.go (about)

     1  package schema
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"log"
     7  	"strconv"
     8  
     9  	"github.com/hashicorp/terraform/config"
    10  	"github.com/hashicorp/terraform/terraform"
    11  )
    12  
    13  // Resource represents a thing in Terraform that has a set of configurable
    14  // attributes and a lifecycle (create, read, update, delete).
    15  //
    16  // The Resource schema is an abstraction that allows provider writers to
    17  // worry only about CRUD operations while off-loading validation, diff
    18  // generation, etc. to this higher level library.
    19  //
    20  // In spite of the name, this struct is not used only for terraform resources,
    21  // but also for data sources. In the case of data sources, the Create,
    22  // Update and Delete functions must not be provided.
    23  type Resource struct {
    24  	// Schema is the schema for the configuration of this resource.
    25  	//
    26  	// The keys of this map are the configuration keys, and the values
    27  	// describe the schema of the configuration value.
    28  	//
    29  	// The schema is used to represent both configurable data as well
    30  	// as data that might be computed in the process of creating this
    31  	// resource.
    32  	Schema map[string]*Schema
    33  
    34  	// SchemaVersion is the version number for this resource's Schema
    35  	// definition. The current SchemaVersion stored in the state for each
    36  	// resource. Provider authors can increment this version number
    37  	// when Schema semantics change. If the State's SchemaVersion is less than
    38  	// the current SchemaVersion, the InstanceState is yielded to the
    39  	// MigrateState callback, where the provider can make whatever changes it
    40  	// needs to update the state to be compatible to the latest version of the
    41  	// Schema.
    42  	//
    43  	// When unset, SchemaVersion defaults to 0, so provider authors can start
    44  	// their Versioning at any integer >= 1
    45  	SchemaVersion int
    46  
    47  	// MigrateState is responsible for updating an InstanceState with an old
    48  	// version to the format expected by the current version of the Schema.
    49  	//
    50  	// It is called during Refresh if the State's stored SchemaVersion is less
    51  	// than the current SchemaVersion of the Resource.
    52  	//
    53  	// The function is yielded the state's stored SchemaVersion and a pointer to
    54  	// the InstanceState that needs updating, as well as the configured
    55  	// provider's configured meta interface{}, in case the migration process
    56  	// needs to make any remote API calls.
    57  	MigrateState StateMigrateFunc
    58  
    59  	// The functions below are the CRUD operations for this resource.
    60  	//
    61  	// The only optional operation is Update. If Update is not implemented,
    62  	// then updates will not be supported for this resource.
    63  	//
    64  	// The ResourceData parameter in the functions below are used to
    65  	// query configuration and changes for the resource as well as to set
    66  	// the ID, computed data, etc.
    67  	//
    68  	// The interface{} parameter is the result of the ConfigureFunc in
    69  	// the provider for this resource. If the provider does not define
    70  	// a ConfigureFunc, this will be nil. This parameter should be used
    71  	// to store API clients, configuration structures, etc.
    72  	//
    73  	// If any errors occur during each of the operation, an error should be
    74  	// returned. If a resource was partially updated, be careful to enable
    75  	// partial state mode for ResourceData and use it accordingly.
    76  	//
    77  	// Exists is a function that is called to check if a resource still
    78  	// exists. If this returns false, then this will affect the diff
    79  	// accordingly. If this function isn't set, it will not be called. It
    80  	// is highly recommended to set it. The *ResourceData passed to Exists
    81  	// should _not_ be modified.
    82  	Create CreateFunc
    83  	Read   ReadFunc
    84  	Update UpdateFunc
    85  	Delete DeleteFunc
    86  	Exists ExistsFunc
    87  
    88  	// CustomizeDiff is a custom function for working with the diff that
    89  	// Terraform has created for this resource - it can be used to customize the
    90  	// diff that has been created, diff values not controlled by configuration,
    91  	// or even veto the diff altogether and abort the plan. It is passed a
    92  	// *ResourceDiff, a structure similar to ResourceData but lacking most write
    93  	// functions like Set, while introducing new functions that work with the
    94  	// diff such as SetNew, SetNewComputed, and ForceNew.
    95  	//
    96  	// The phases Terraform runs this in, and the state available via functions
    97  	// like Get and GetChange, are as follows:
    98  	//
    99  	//  * New resource: One run with no state
   100  	//  * Existing resource: One run with state
   101  	//   * Existing resource, forced new: One run with state (before ForceNew),
   102  	//     then one run without state (as if new resource)
   103  	//  * Tainted resource: No runs (custom diff logic is skipped)
   104  	//  * Destroy: No runs (standard diff logic is skipped on destroy diffs)
   105  	//
   106  	// This function needs to be resilient to support all scenarios.
   107  	//
   108  	// If this function needs to access external API resources, remember to flag
   109  	// the RequiresRefresh attribute mentioned below to ensure that
   110  	// -refresh=false is blocked when running plan or apply, as this means that
   111  	// this resource requires refresh-like behaviour to work effectively.
   112  	//
   113  	// For the most part, only computed fields can be customized by this
   114  	// function.
   115  	//
   116  	// This function is only allowed on regular resources (not data sources).
   117  	CustomizeDiff CustomizeDiffFunc
   118  
   119  	// Importer is the ResourceImporter implementation for this resource.
   120  	// If this is nil, then this resource does not support importing. If
   121  	// this is non-nil, then it supports importing and ResourceImporter
   122  	// must be validated. The validity of ResourceImporter is verified
   123  	// by InternalValidate on Resource.
   124  	Importer *ResourceImporter
   125  
   126  	// If non-empty, this string is emitted as a warning during Validate.
   127  	// This is a private interface for now, for use by DataSourceResourceShim,
   128  	// and not for general use. (But maybe later...)
   129  	deprecationMessage string
   130  
   131  	// Timeouts allow users to specify specific time durations in which an
   132  	// operation should time out, to allow them to extend an action to suit their
   133  	// usage. For example, a user may specify a large Creation timeout for their
   134  	// AWS RDS Instance due to it's size, or restoring from a snapshot.
   135  	// Resource implementors must enable Timeout support by adding the allowed
   136  	// actions (Create, Read, Update, Delete, Default) to the Resource struct, and
   137  	// accessing them in the matching methods.
   138  	Timeouts *ResourceTimeout
   139  }
   140  
   141  // See Resource documentation.
   142  type CreateFunc func(*ResourceData, interface{}) error
   143  
   144  // See Resource documentation.
   145  type ReadFunc func(*ResourceData, interface{}) error
   146  
   147  // See Resource documentation.
   148  type UpdateFunc func(*ResourceData, interface{}) error
   149  
   150  // See Resource documentation.
   151  type DeleteFunc func(*ResourceData, interface{}) error
   152  
   153  // See Resource documentation.
   154  type ExistsFunc func(*ResourceData, interface{}) (bool, error)
   155  
   156  // See Resource documentation.
   157  type StateMigrateFunc func(
   158  	int, *terraform.InstanceState, interface{}) (*terraform.InstanceState, error)
   159  
   160  // See Resource documentation.
   161  type CustomizeDiffFunc func(*ResourceDiff, interface{}) error
   162  
   163  // Apply creates, updates, and/or deletes a resource.
   164  func (r *Resource) Apply(
   165  	s *terraform.InstanceState,
   166  	d *terraform.InstanceDiff,
   167  	meta interface{}) (*terraform.InstanceState, error) {
   168  	data, err := schemaMap(r.Schema).Data(s, d)
   169  	if err != nil {
   170  		return s, err
   171  	}
   172  
   173  	// Instance Diff shoould have the timeout info, need to copy it over to the
   174  	// ResourceData meta
   175  	rt := ResourceTimeout{}
   176  	if _, ok := d.Meta[TimeoutKey]; ok {
   177  		if err := rt.DiffDecode(d); err != nil {
   178  			log.Printf("[ERR] Error decoding ResourceTimeout: %s", err)
   179  		}
   180  	} else if s != nil {
   181  		if _, ok := s.Meta[TimeoutKey]; ok {
   182  			if err := rt.StateDecode(s); err != nil {
   183  				log.Printf("[ERR] Error decoding ResourceTimeout: %s", err)
   184  			}
   185  		}
   186  	} else {
   187  		log.Printf("[DEBUG] No meta timeoutkey found in Apply()")
   188  	}
   189  	data.timeouts = &rt
   190  
   191  	if s == nil {
   192  		// The Terraform API dictates that this should never happen, but
   193  		// it doesn't hurt to be safe in this case.
   194  		s = new(terraform.InstanceState)
   195  	}
   196  
   197  	if d.Destroy || d.RequiresNew() {
   198  		if s.ID != "" {
   199  			// Destroy the resource since it is created
   200  			if err := r.Delete(data, meta); err != nil {
   201  				return r.recordCurrentSchemaVersion(data.State()), err
   202  			}
   203  
   204  			// Make sure the ID is gone.
   205  			data.SetId("")
   206  		}
   207  
   208  		// If we're only destroying, and not creating, then return
   209  		// now since we're done!
   210  		if !d.RequiresNew() {
   211  			return nil, nil
   212  		}
   213  
   214  		// Reset the data to be stateless since we just destroyed
   215  		data, err = schemaMap(r.Schema).Data(nil, d)
   216  		// data was reset, need to re-apply the parsed timeouts
   217  		data.timeouts = &rt
   218  		if err != nil {
   219  			return nil, err
   220  		}
   221  	}
   222  
   223  	err = nil
   224  	if data.Id() == "" {
   225  		// We're creating, it is a new resource.
   226  		data.MarkNewResource()
   227  		err = r.Create(data, meta)
   228  	} else {
   229  		if r.Update == nil {
   230  			return s, fmt.Errorf("doesn't support update")
   231  		}
   232  
   233  		err = r.Update(data, meta)
   234  	}
   235  
   236  	return r.recordCurrentSchemaVersion(data.State()), err
   237  }
   238  
   239  // Diff returns a diff of this resource.
   240  func (r *Resource) Diff(
   241  	s *terraform.InstanceState,
   242  	c *terraform.ResourceConfig,
   243  	meta interface{}) (*terraform.InstanceDiff, error) {
   244  
   245  	t := &ResourceTimeout{}
   246  	err := t.ConfigDecode(r, c)
   247  
   248  	if err != nil {
   249  		return nil, fmt.Errorf("[ERR] Error decoding timeout: %s", err)
   250  	}
   251  
   252  	instanceDiff, err := schemaMap(r.Schema).Diff(s, c, r.CustomizeDiff, meta)
   253  	if err != nil {
   254  		return instanceDiff, err
   255  	}
   256  
   257  	if instanceDiff != nil {
   258  		if err := t.DiffEncode(instanceDiff); err != nil {
   259  			log.Printf("[ERR] Error encoding timeout to instance diff: %s", err)
   260  		}
   261  	} else {
   262  		log.Printf("[DEBUG] Instance Diff is nil in Diff()")
   263  	}
   264  
   265  	return instanceDiff, err
   266  }
   267  
   268  // Validate validates the resource configuration against the schema.
   269  func (r *Resource) Validate(c *terraform.ResourceConfig) ([]string, []error) {
   270  	warns, errs := schemaMap(r.Schema).Validate(c)
   271  
   272  	if r.deprecationMessage != "" {
   273  		warns = append(warns, r.deprecationMessage)
   274  	}
   275  
   276  	return warns, errs
   277  }
   278  
   279  // ReadDataApply loads the data for a data source, given a diff that
   280  // describes the configuration arguments and desired computed attributes.
   281  func (r *Resource) ReadDataApply(
   282  	d *terraform.InstanceDiff,
   283  	meta interface{},
   284  ) (*terraform.InstanceState, error) {
   285  	// Data sources are always built completely from scratch
   286  	// on each read, so the source state is always nil.
   287  	data, err := schemaMap(r.Schema).Data(nil, d)
   288  	if err != nil {
   289  		return nil, err
   290  	}
   291  
   292  	err = r.Read(data, meta)
   293  	state := data.State()
   294  	if state != nil && state.ID == "" {
   295  		// Data sources can set an ID if they want, but they aren't
   296  		// required to; we'll provide a placeholder if they don't,
   297  		// to preserve the invariant that all resources have non-empty
   298  		// ids.
   299  		state.ID = "-"
   300  	}
   301  
   302  	return r.recordCurrentSchemaVersion(state), err
   303  }
   304  
   305  // Refresh refreshes the state of the resource.
   306  func (r *Resource) Refresh(
   307  	s *terraform.InstanceState,
   308  	meta interface{}) (*terraform.InstanceState, error) {
   309  	// If the ID is already somehow blank, it doesn't exist
   310  	if s.ID == "" {
   311  		return nil, nil
   312  	}
   313  
   314  	rt := ResourceTimeout{}
   315  	if _, ok := s.Meta[TimeoutKey]; ok {
   316  		if err := rt.StateDecode(s); err != nil {
   317  			log.Printf("[ERR] Error decoding ResourceTimeout: %s", err)
   318  		}
   319  	}
   320  
   321  	if r.Exists != nil {
   322  		// Make a copy of data so that if it is modified it doesn't
   323  		// affect our Read later.
   324  		data, err := schemaMap(r.Schema).Data(s, nil)
   325  		data.timeouts = &rt
   326  
   327  		if err != nil {
   328  			return s, err
   329  		}
   330  
   331  		exists, err := r.Exists(data, meta)
   332  		if err != nil {
   333  			return s, err
   334  		}
   335  		if !exists {
   336  			return nil, nil
   337  		}
   338  	}
   339  
   340  	needsMigration, stateSchemaVersion := r.checkSchemaVersion(s)
   341  	if needsMigration && r.MigrateState != nil {
   342  		s, err := r.MigrateState(stateSchemaVersion, s, meta)
   343  		if err != nil {
   344  			return s, err
   345  		}
   346  	}
   347  
   348  	data, err := schemaMap(r.Schema).Data(s, nil)
   349  	data.timeouts = &rt
   350  	if err != nil {
   351  		return s, err
   352  	}
   353  
   354  	err = r.Read(data, meta)
   355  	state := data.State()
   356  	if state != nil && state.ID == "" {
   357  		state = nil
   358  	}
   359  
   360  	return r.recordCurrentSchemaVersion(state), err
   361  }
   362  
   363  // InternalValidate should be called to validate the structure
   364  // of the resource.
   365  //
   366  // This should be called in a unit test for any resource to verify
   367  // before release that a resource is properly configured for use with
   368  // this library.
   369  //
   370  // Provider.InternalValidate() will automatically call this for all of
   371  // the resources it manages, so you don't need to call this manually if it
   372  // is part of a Provider.
   373  func (r *Resource) InternalValidate(topSchemaMap schemaMap, writable bool) error {
   374  	if r == nil {
   375  		return errors.New("resource is nil")
   376  	}
   377  
   378  	if !writable {
   379  		if r.Create != nil || r.Update != nil || r.Delete != nil {
   380  			return fmt.Errorf("must not implement Create, Update or Delete")
   381  		}
   382  
   383  		// CustomizeDiff cannot be defined for read-only resources
   384  		if r.CustomizeDiff != nil {
   385  			return fmt.Errorf("cannot implement CustomizeDiff")
   386  		}
   387  	}
   388  
   389  	tsm := topSchemaMap
   390  
   391  	if r.isTopLevel() && writable {
   392  		// All non-Computed attributes must be ForceNew if Update is not defined
   393  		if r.Update == nil {
   394  			nonForceNewAttrs := make([]string, 0)
   395  			for k, v := range r.Schema {
   396  				if !v.ForceNew && !v.Computed {
   397  					nonForceNewAttrs = append(nonForceNewAttrs, k)
   398  				}
   399  			}
   400  			if len(nonForceNewAttrs) > 0 {
   401  				return fmt.Errorf(
   402  					"No Update defined, must set ForceNew on: %#v", nonForceNewAttrs)
   403  			}
   404  		} else {
   405  			nonUpdateableAttrs := make([]string, 0)
   406  			for k, v := range r.Schema {
   407  				if v.ForceNew || v.Computed && !v.Optional {
   408  					nonUpdateableAttrs = append(nonUpdateableAttrs, k)
   409  				}
   410  			}
   411  			updateableAttrs := len(r.Schema) - len(nonUpdateableAttrs)
   412  			if updateableAttrs == 0 {
   413  				return fmt.Errorf(
   414  					"All fields are ForceNew or Computed w/out Optional, Update is superfluous")
   415  			}
   416  		}
   417  
   418  		tsm = schemaMap(r.Schema)
   419  
   420  		// Destroy, and Read are required
   421  		if r.Read == nil {
   422  			return fmt.Errorf("Read must be implemented")
   423  		}
   424  		if r.Delete == nil {
   425  			return fmt.Errorf("Delete must be implemented")
   426  		}
   427  
   428  		// If we have an importer, we need to verify the importer.
   429  		if r.Importer != nil {
   430  			if err := r.Importer.InternalValidate(); err != nil {
   431  				return err
   432  			}
   433  		}
   434  
   435  		for k, f := range tsm {
   436  			if isReservedResourceFieldName(k, f) {
   437  				return fmt.Errorf("%s is a reserved field name", k)
   438  			}
   439  		}
   440  	}
   441  
   442  	// Data source
   443  	if r.isTopLevel() && !writable {
   444  		tsm = schemaMap(r.Schema)
   445  		for k, _ := range tsm {
   446  			if isReservedDataSourceFieldName(k) {
   447  				return fmt.Errorf("%s is a reserved field name", k)
   448  			}
   449  		}
   450  	}
   451  
   452  	return schemaMap(r.Schema).InternalValidate(tsm)
   453  }
   454  
   455  func isReservedDataSourceFieldName(name string) bool {
   456  	for _, reservedName := range config.ReservedDataSourceFields {
   457  		if name == reservedName {
   458  			return true
   459  		}
   460  	}
   461  	return false
   462  }
   463  
   464  func isReservedResourceFieldName(name string, s *Schema) bool {
   465  	// Allow phasing out "id"
   466  	// See https://github.com/terraform-providers/terraform-provider-aws/pull/1626#issuecomment-328881415
   467  	if name == "id" && (s.Deprecated != "" || s.Removed != "") {
   468  		return false
   469  	}
   470  
   471  	for _, reservedName := range config.ReservedResourceFields {
   472  		if name == reservedName {
   473  			return true
   474  		}
   475  	}
   476  	return false
   477  }
   478  
   479  // Data returns a ResourceData struct for this Resource. Each return value
   480  // is a separate copy and can be safely modified differently.
   481  //
   482  // The data returned from this function has no actual affect on the Resource
   483  // itself (including the state given to this function).
   484  //
   485  // This function is useful for unit tests and ResourceImporter functions.
   486  func (r *Resource) Data(s *terraform.InstanceState) *ResourceData {
   487  	result, err := schemaMap(r.Schema).Data(s, nil)
   488  	if err != nil {
   489  		// At the time of writing, this isn't possible (Data never returns
   490  		// non-nil errors). We panic to find this in the future if we have to.
   491  		// I don't see a reason for Data to ever return an error.
   492  		panic(err)
   493  	}
   494  
   495  	// load the Resource timeouts
   496  	result.timeouts = r.Timeouts
   497  	if result.timeouts == nil {
   498  		result.timeouts = &ResourceTimeout{}
   499  	}
   500  
   501  	// Set the schema version to latest by default
   502  	result.meta = map[string]interface{}{
   503  		"schema_version": strconv.Itoa(r.SchemaVersion),
   504  	}
   505  
   506  	return result
   507  }
   508  
   509  // TestResourceData Yields a ResourceData filled with this resource's schema for use in unit testing
   510  //
   511  // TODO: May be able to be removed with the above ResourceData function.
   512  func (r *Resource) TestResourceData() *ResourceData {
   513  	return &ResourceData{
   514  		schema: r.Schema,
   515  	}
   516  }
   517  
   518  // Returns true if the resource is "top level" i.e. not a sub-resource.
   519  func (r *Resource) isTopLevel() bool {
   520  	// TODO: This is a heuristic; replace with a definitive attribute?
   521  	return (r.Create != nil || r.Read != nil)
   522  }
   523  
   524  // Determines if a given InstanceState needs to be migrated by checking the
   525  // stored version number with the current SchemaVersion
   526  func (r *Resource) checkSchemaVersion(is *terraform.InstanceState) (bool, int) {
   527  	// Get the raw interface{} value for the schema version. If it doesn't
   528  	// exist or is nil then set it to zero.
   529  	raw := is.Meta["schema_version"]
   530  	if raw == nil {
   531  		raw = "0"
   532  	}
   533  
   534  	// Try to convert it to a string. If it isn't a string then we pretend
   535  	// that it isn't set at all. It should never not be a string unless it
   536  	// was manually tampered with.
   537  	rawString, ok := raw.(string)
   538  	if !ok {
   539  		rawString = "0"
   540  	}
   541  
   542  	stateSchemaVersion, _ := strconv.Atoi(rawString)
   543  	return stateSchemaVersion < r.SchemaVersion, stateSchemaVersion
   544  }
   545  
   546  func (r *Resource) recordCurrentSchemaVersion(
   547  	state *terraform.InstanceState) *terraform.InstanceState {
   548  	if state != nil && r.SchemaVersion > 0 {
   549  		if state.Meta == nil {
   550  			state.Meta = make(map[string]interface{})
   551  		}
   552  		state.Meta["schema_version"] = strconv.Itoa(r.SchemaVersion)
   553  	}
   554  	return state
   555  }
   556  
   557  // Noop is a convenience implementation of resource function which takes
   558  // no action and returns no error.
   559  func Noop(*ResourceData, interface{}) error {
   560  	return nil
   561  }
   562  
   563  // RemoveFromState is a convenience implementation of a resource function
   564  // which sets the resource ID to empty string (to remove it from state)
   565  // and returns no error.
   566  func RemoveFromState(d *ResourceData, _ interface{}) error {
   567  	d.SetId("")
   568  	return nil
   569  }