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