github.com/bigkraig/terraform@v0.6.4-0.20151219155159-c90d1b074e31/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  type Resource struct {
    18  	// Schema is the schema for the configuration of this resource.
    19  	//
    20  	// The keys of this map are the configuration keys, and the values
    21  	// describe the schema of the configuration value.
    22  	//
    23  	// The schema is used to represent both configurable data as well
    24  	// as data that might be computed in the process of creating this
    25  	// resource.
    26  	Schema map[string]*Schema
    27  
    28  	// SchemaVersion is the version number for this resource's Schema
    29  	// definition. The current SchemaVersion stored in the state for each
    30  	// resource. Provider authors can increment this version number
    31  	// when Schema semantics change. If the State's SchemaVersion is less than
    32  	// the current SchemaVersion, the InstanceState is yielded to the
    33  	// MigrateState callback, where the provider can make whatever changes it
    34  	// needs to update the state to be compatible to the latest version of the
    35  	// Schema.
    36  	//
    37  	// When unset, SchemaVersion defaults to 0, so provider authors can start
    38  	// their Versioning at any integer >= 1
    39  	SchemaVersion int
    40  
    41  	// MigrateState is responsible for updating an InstanceState with an old
    42  	// version to the format expected by the current version of the Schema.
    43  	//
    44  	// It is called during Refresh if the State's stored SchemaVersion is less
    45  	// than the current SchemaVersion of the Resource.
    46  	//
    47  	// The function is yielded the state's stored SchemaVersion and a pointer to
    48  	// the InstanceState that needs updating, as well as the configured
    49  	// provider's configured meta interface{}, in case the migration process
    50  	// needs to make any remote API calls.
    51  	MigrateState StateMigrateFunc
    52  
    53  	// The functions below are the CRUD operations for this resource.
    54  	//
    55  	// The only optional operation is Update. If Update is not implemented,
    56  	// then updates will not be supported for this resource.
    57  	//
    58  	// The ResourceData parameter in the functions below are used to
    59  	// query configuration and changes for the resource as well as to set
    60  	// the ID, computed data, etc.
    61  	//
    62  	// The interface{} parameter is the result of the ConfigureFunc in
    63  	// the provider for this resource. If the provider does not define
    64  	// a ConfigureFunc, this will be nil. This parameter should be used
    65  	// to store API clients, configuration structures, etc.
    66  	//
    67  	// If any errors occur during each of the operation, an error should be
    68  	// returned. If a resource was partially updated, be careful to enable
    69  	// partial state mode for ResourceData and use it accordingly.
    70  	//
    71  	// Exists is a function that is called to check if a resource still
    72  	// exists. If this returns false, then this will affect the diff
    73  	// accordingly. If this function isn't set, it will not be called. It
    74  	// is highly recommended to set it. The *ResourceData passed to Exists
    75  	// should _not_ be modified.
    76  	Create CreateFunc
    77  	Read   ReadFunc
    78  	Update UpdateFunc
    79  	Delete DeleteFunc
    80  	Exists ExistsFunc
    81  }
    82  
    83  // See Resource documentation.
    84  type CreateFunc func(*ResourceData, interface{}) error
    85  
    86  // See Resource documentation.
    87  type ReadFunc func(*ResourceData, interface{}) error
    88  
    89  // See Resource documentation.
    90  type UpdateFunc func(*ResourceData, interface{}) error
    91  
    92  // See Resource documentation.
    93  type DeleteFunc func(*ResourceData, interface{}) error
    94  
    95  // See Resource documentation.
    96  type ExistsFunc func(*ResourceData, interface{}) (bool, error)
    97  
    98  // See Resource documentation.
    99  type StateMigrateFunc func(
   100  	int, *terraform.InstanceState, interface{}) (*terraform.InstanceState, error)
   101  
   102  // Apply creates, updates, and/or deletes a resource.
   103  func (r *Resource) Apply(
   104  	s *terraform.InstanceState,
   105  	d *terraform.InstanceDiff,
   106  	meta interface{}) (*terraform.InstanceState, error) {
   107  	data, err := schemaMap(r.Schema).Data(s, d)
   108  	if err != nil {
   109  		return s, err
   110  	}
   111  
   112  	if s == nil {
   113  		// The Terraform API dictates that this should never happen, but
   114  		// it doesn't hurt to be safe in this case.
   115  		s = new(terraform.InstanceState)
   116  	}
   117  
   118  	if d.Destroy || d.RequiresNew() {
   119  		if s.ID != "" {
   120  			// Destroy the resource since it is created
   121  			if err := r.Delete(data, meta); err != nil {
   122  				return r.recordCurrentSchemaVersion(data.State()), err
   123  			}
   124  
   125  			// Make sure the ID is gone.
   126  			data.SetId("")
   127  		}
   128  
   129  		// If we're only destroying, and not creating, then return
   130  		// now since we're done!
   131  		if !d.RequiresNew() {
   132  			return nil, nil
   133  		}
   134  
   135  		// Reset the data to be stateless since we just destroyed
   136  		data, err = schemaMap(r.Schema).Data(nil, d)
   137  		if err != nil {
   138  			return nil, err
   139  		}
   140  	}
   141  
   142  	err = nil
   143  	if data.Id() == "" {
   144  		// We're creating, it is a new resource.
   145  		err = r.Create(data, meta)
   146  	} else {
   147  		if r.Update == nil {
   148  			return s, fmt.Errorf("doesn't support update")
   149  		}
   150  
   151  		err = r.Update(data, meta)
   152  	}
   153  
   154  	return r.recordCurrentSchemaVersion(data.State()), err
   155  }
   156  
   157  // Diff returns a diff of this resource and is API compatible with the
   158  // ResourceProvider interface.
   159  func (r *Resource) Diff(
   160  	s *terraform.InstanceState,
   161  	c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) {
   162  	return schemaMap(r.Schema).Diff(s, c)
   163  }
   164  
   165  // Validate validates the resource configuration against the schema.
   166  func (r *Resource) Validate(c *terraform.ResourceConfig) ([]string, []error) {
   167  	return schemaMap(r.Schema).Validate(c)
   168  }
   169  
   170  // Refresh refreshes the state of the resource.
   171  func (r *Resource) Refresh(
   172  	s *terraform.InstanceState,
   173  	meta interface{}) (*terraform.InstanceState, error) {
   174  	// If the ID is already somehow blank, it doesn't exist
   175  	if s.ID == "" {
   176  		return nil, nil
   177  	}
   178  
   179  	if r.Exists != nil {
   180  		// Make a copy of data so that if it is modified it doesn't
   181  		// affect our Read later.
   182  		data, err := schemaMap(r.Schema).Data(s, nil)
   183  		if err != nil {
   184  			return s, err
   185  		}
   186  
   187  		exists, err := r.Exists(data, meta)
   188  		if err != nil {
   189  			return s, err
   190  		}
   191  		if !exists {
   192  			return nil, nil
   193  		}
   194  	}
   195  
   196  	needsMigration, stateSchemaVersion := r.checkSchemaVersion(s)
   197  	if needsMigration && r.MigrateState != nil {
   198  		s, err := r.MigrateState(stateSchemaVersion, s, meta)
   199  		if err != nil {
   200  			return s, err
   201  		}
   202  	}
   203  
   204  	data, err := schemaMap(r.Schema).Data(s, nil)
   205  	if err != nil {
   206  		return s, err
   207  	}
   208  
   209  	err = r.Read(data, meta)
   210  	state := data.State()
   211  	if state != nil && state.ID == "" {
   212  		state = nil
   213  	}
   214  
   215  	return r.recordCurrentSchemaVersion(state), err
   216  }
   217  
   218  // InternalValidate should be called to validate the structure
   219  // of the resource.
   220  //
   221  // This should be called in a unit test for any resource to verify
   222  // before release that a resource is properly configured for use with
   223  // this library.
   224  //
   225  // Provider.InternalValidate() will automatically call this for all of
   226  // the resources it manages, so you don't need to call this manually if it
   227  // is part of a Provider.
   228  func (r *Resource) InternalValidate(topSchemaMap schemaMap) error {
   229  	if r == nil {
   230  		return errors.New("resource is nil")
   231  	}
   232  	tsm := topSchemaMap
   233  
   234  	if r.isTopLevel() {
   235  		// All non-Computed attributes must be ForceNew if Update is not defined
   236  		if r.Update == nil {
   237  			nonForceNewAttrs := make([]string, 0)
   238  			for k, v := range r.Schema {
   239  				if !v.ForceNew && !v.Computed {
   240  					nonForceNewAttrs = append(nonForceNewAttrs, k)
   241  				}
   242  			}
   243  			if len(nonForceNewAttrs) > 0 {
   244  				return fmt.Errorf(
   245  					"No Update defined, must set ForceNew on: %#v", nonForceNewAttrs)
   246  			}
   247  		} else {
   248  			nonUpdateableAttrs := make([]string, 0)
   249  			for k, v := range r.Schema {
   250  				if v.ForceNew || v.Computed && !v.Optional {
   251  					nonUpdateableAttrs = append(nonUpdateableAttrs, k)
   252  				}
   253  			}
   254  			updateableAttrs := len(r.Schema) - len(nonUpdateableAttrs)
   255  			if updateableAttrs == 0 {
   256  				return fmt.Errorf(
   257  					"All fields are ForceNew or Computed w/out Optional, Update is superfluous")
   258  			}
   259  		}
   260  
   261  		tsm = schemaMap(r.Schema)
   262  	}
   263  
   264  	return schemaMap(r.Schema).InternalValidate(tsm)
   265  }
   266  
   267  // Returns true if the resource is "top level" i.e. not a sub-resource.
   268  func (r *Resource) isTopLevel() bool {
   269  	// TODO: This is a heuristic; replace with a definitive attribute?
   270  	return r.Create != nil
   271  }
   272  
   273  // Determines if a given InstanceState needs to be migrated by checking the
   274  // stored version number with the current SchemaVersion
   275  func (r *Resource) checkSchemaVersion(is *terraform.InstanceState) (bool, int) {
   276  	stateSchemaVersion, _ := strconv.Atoi(is.Meta["schema_version"])
   277  	return stateSchemaVersion < r.SchemaVersion, stateSchemaVersion
   278  }
   279  
   280  func (r *Resource) recordCurrentSchemaVersion(
   281  	state *terraform.InstanceState) *terraform.InstanceState {
   282  	if state != nil && r.SchemaVersion > 0 {
   283  		if state.Meta == nil {
   284  			state.Meta = make(map[string]string)
   285  		}
   286  		state.Meta["schema_version"] = strconv.Itoa(r.SchemaVersion)
   287  	}
   288  	return state
   289  }