github.com/nevins-b/terraform@v0.3.8-0.20170215184714-bbae22007d5a/helper/schema/resource.go (about)

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