1package schema
2
3import (
4	"errors"
5	"fmt"
6	"log"
7	"strconv"
8
9	"github.com/hashicorp/terraform/config"
10	"github.com/hashicorp/terraform/terraform"
11	"github.com/zclconf/go-cty/cty"
12)
13
14// Resource represents a thing in Terraform that has a set of configurable
15// attributes and a lifecycle (create, read, update, delete).
16//
17// The Resource schema is an abstraction that allows provider writers to
18// worry only about CRUD operations while off-loading validation, diff
19// generation, etc. to this higher level library.
20//
21// In spite of the name, this struct is not used only for terraform resources,
22// but also for data sources. In the case of data sources, the Create,
23// Update and Delete functions must not be provided.
24type Resource struct {
25	// Schema is the schema for the configuration of this resource.
26	//
27	// The keys of this map are the configuration keys, and the values
28	// describe the schema of the configuration value.
29	//
30	// The schema is used to represent both configurable data as well
31	// as data that might be computed in the process of creating this
32	// resource.
33	Schema map[string]*Schema
34
35	// SchemaVersion is the version number for this resource's Schema
36	// definition. The current SchemaVersion stored in the state for each
37	// resource. Provider authors can increment this version number
38	// when Schema semantics change. If the State's SchemaVersion is less than
39	// the current SchemaVersion, the InstanceState is yielded to the
40	// MigrateState callback, where the provider can make whatever changes it
41	// needs to update the state to be compatible to the latest version of the
42	// Schema.
43	//
44	// When unset, SchemaVersion defaults to 0, so provider authors can start
45	// their Versioning at any integer >= 1
46	SchemaVersion int
47
48	// MigrateState is deprecated and any new changes to a resource's schema
49	// should be handled by StateUpgraders. Existing MigrateState implementations
50	// should remain for compatibility with existing state. MigrateState will
51	// still be called if the stored SchemaVersion is less than the
52	// first version of the StateUpgraders.
53	//
54	// MigrateState is responsible for updating an InstanceState with an old
55	// version to the format expected by the current version of the Schema.
56	//
57	// It is called during Refresh if the State's stored SchemaVersion is less
58	// than the current SchemaVersion of the Resource.
59	//
60	// The function is yielded the state's stored SchemaVersion and a pointer to
61	// the InstanceState that needs updating, as well as the configured
62	// provider's configured meta interface{}, in case the migration process
63	// needs to make any remote API calls.
64	MigrateState StateMigrateFunc
65
66	// StateUpgraders contains the functions responsible for upgrading an
67	// existing state with an old schema version to a newer schema. It is
68	// called specifically by Terraform when the stored schema version is less
69	// than the current SchemaVersion of the Resource.
70	//
71	// StateUpgraders map specific schema versions to a StateUpgrader
72	// function. The registered versions are expected to be ordered,
73	// consecutive values. The initial value may be greater than 0 to account
74	// for legacy schemas that weren't recorded and can be handled by
75	// MigrateState.
76	StateUpgraders []StateUpgrader
77
78	// The functions below are the CRUD operations for this resource.
79	//
80	// The only optional operation is Update. If Update is not implemented,
81	// then updates will not be supported for this resource.
82	//
83	// The ResourceData parameter in the functions below are used to
84	// query configuration and changes for the resource as well as to set
85	// the ID, computed data, etc.
86	//
87	// The interface{} parameter is the result of the ConfigureFunc in
88	// the provider for this resource. If the provider does not define
89	// a ConfigureFunc, this will be nil. This parameter should be used
90	// to store API clients, configuration structures, etc.
91	//
92	// If any errors occur during each of the operation, an error should be
93	// returned. If a resource was partially updated, be careful to enable
94	// partial state mode for ResourceData and use it accordingly.
95	//
96	// Exists is a function that is called to check if a resource still
97	// exists. If this returns false, then this will affect the diff
98	// accordingly. If this function isn't set, it will not be called. It
99	// is highly recommended to set it. The *ResourceData passed to Exists
100	// should _not_ be modified.
101	Create CreateFunc
102	Read   ReadFunc
103	Update UpdateFunc
104	Delete DeleteFunc
105	Exists ExistsFunc
106
107	// CustomizeDiff is a custom function for working with the diff that
108	// Terraform has created for this resource - it can be used to customize the
109	// diff that has been created, diff values not controlled by configuration,
110	// or even veto the diff altogether and abort the plan. It is passed a
111	// *ResourceDiff, a structure similar to ResourceData but lacking most write
112	// functions like Set, while introducing new functions that work with the
113	// diff such as SetNew, SetNewComputed, and ForceNew.
114	//
115	// The phases Terraform runs this in, and the state available via functions
116	// like Get and GetChange, are as follows:
117	//
118	//  * New resource: One run with no state
119	//  * Existing resource: One run with state
120	//   * Existing resource, forced new: One run with state (before ForceNew),
121	//     then one run without state (as if new resource)
122	//  * Tainted resource: No runs (custom diff logic is skipped)
123	//  * Destroy: No runs (standard diff logic is skipped on destroy diffs)
124	//
125	// This function needs to be resilient to support all scenarios.
126	//
127	// If this function needs to access external API resources, remember to flag
128	// the RequiresRefresh attribute mentioned below to ensure that
129	// -refresh=false is blocked when running plan or apply, as this means that
130	// this resource requires refresh-like behaviour to work effectively.
131	//
132	// For the most part, only computed fields can be customized by this
133	// function.
134	//
135	// This function is only allowed on regular resources (not data sources).
136	CustomizeDiff CustomizeDiffFunc
137
138	// Importer is the ResourceImporter implementation for this resource.
139	// If this is nil, then this resource does not support importing. If
140	// this is non-nil, then it supports importing and ResourceImporter
141	// must be validated. The validity of ResourceImporter is verified
142	// by InternalValidate on Resource.
143	Importer *ResourceImporter
144
145	// If non-empty, this string is emitted as a warning during Validate.
146	DeprecationMessage string
147
148	// Timeouts allow users to specify specific time durations in which an
149	// operation should time out, to allow them to extend an action to suit their
150	// usage. For example, a user may specify a large Creation timeout for their
151	// AWS RDS Instance due to it's size, or restoring from a snapshot.
152	// Resource implementors must enable Timeout support by adding the allowed
153	// actions (Create, Read, Update, Delete, Default) to the Resource struct, and
154	// accessing them in the matching methods.
155	Timeouts *ResourceTimeout
156}
157
158// ShimInstanceStateFromValue converts a cty.Value to a
159// terraform.InstanceState.
160func (r *Resource) ShimInstanceStateFromValue(state cty.Value) (*terraform.InstanceState, error) {
161	// Get the raw shimmed value. While this is correct, the set hashes don't
162	// match those from the Schema.
163	s := terraform.NewInstanceStateShimmedFromValue(state, r.SchemaVersion)
164
165	// We now rebuild the state through the ResourceData, so that the set indexes
166	// match what helper/schema expects.
167	data, err := schemaMap(r.Schema).Data(s, nil)
168	if err != nil {
169		return nil, err
170	}
171
172	s = data.State()
173	if s == nil {
174		s = &terraform.InstanceState{}
175	}
176	return s, nil
177}
178
179// See Resource documentation.
180type CreateFunc func(*ResourceData, interface{}) error
181
182// See Resource documentation.
183type ReadFunc func(*ResourceData, interface{}) error
184
185// See Resource documentation.
186type UpdateFunc func(*ResourceData, interface{}) error
187
188// See Resource documentation.
189type DeleteFunc func(*ResourceData, interface{}) error
190
191// See Resource documentation.
192type ExistsFunc func(*ResourceData, interface{}) (bool, error)
193
194// See Resource documentation.
195type StateMigrateFunc func(
196	int, *terraform.InstanceState, interface{}) (*terraform.InstanceState, error)
197
198type StateUpgrader struct {
199	// Version is the version schema that this Upgrader will handle, converting
200	// it to Version+1.
201	Version int
202
203	// Type describes the schema that this function can upgrade. Type is
204	// required to decode the schema if the state was stored in a legacy
205	// flatmap format.
206	Type cty.Type
207
208	// Upgrade takes the JSON encoded state and the provider meta value, and
209	// upgrades the state one single schema version. The provided state is
210	// deocded into the default json types using a map[string]interface{}. It
211	// is up to the StateUpgradeFunc to ensure that the returned value can be
212	// encoded using the new schema.
213	Upgrade StateUpgradeFunc
214}
215
216// See StateUpgrader
217type StateUpgradeFunc func(rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error)
218
219// See Resource documentation.
220type CustomizeDiffFunc func(*ResourceDiff, interface{}) error
221
222// Apply creates, updates, and/or deletes a resource.
223func (r *Resource) Apply(
224	s *terraform.InstanceState,
225	d *terraform.InstanceDiff,
226	meta interface{}) (*terraform.InstanceState, error) {
227	data, err := schemaMap(r.Schema).Data(s, d)
228	if err != nil {
229		return s, err
230	}
231
232	// Instance Diff shoould have the timeout info, need to copy it over to the
233	// ResourceData meta
234	rt := ResourceTimeout{}
235	if _, ok := d.Meta[TimeoutKey]; ok {
236		if err := rt.DiffDecode(d); err != nil {
237			log.Printf("[ERR] Error decoding ResourceTimeout: %s", err)
238		}
239	} else if s != nil {
240		if _, ok := s.Meta[TimeoutKey]; ok {
241			if err := rt.StateDecode(s); err != nil {
242				log.Printf("[ERR] Error decoding ResourceTimeout: %s", err)
243			}
244		}
245	} else {
246		log.Printf("[DEBUG] No meta timeoutkey found in Apply()")
247	}
248	data.timeouts = &rt
249
250	if s == nil {
251		// The Terraform API dictates that this should never happen, but
252		// it doesn't hurt to be safe in this case.
253		s = new(terraform.InstanceState)
254	}
255
256	if d.Destroy || d.RequiresNew() {
257		if s.ID != "" {
258			// Destroy the resource since it is created
259			if err := r.Delete(data, meta); err != nil {
260				return r.recordCurrentSchemaVersion(data.State()), err
261			}
262
263			// Make sure the ID is gone.
264			data.SetId("")
265		}
266
267		// If we're only destroying, and not creating, then return
268		// now since we're done!
269		if !d.RequiresNew() {
270			return nil, nil
271		}
272
273		// Reset the data to be stateless since we just destroyed
274		data, err = schemaMap(r.Schema).Data(nil, d)
275		// data was reset, need to re-apply the parsed timeouts
276		data.timeouts = &rt
277		if err != nil {
278			return nil, err
279		}
280	}
281
282	err = nil
283	if data.Id() == "" {
284		// We're creating, it is a new resource.
285		data.MarkNewResource()
286		err = r.Create(data, meta)
287	} else {
288		if r.Update == nil {
289			return s, fmt.Errorf("doesn't support update")
290		}
291
292		err = r.Update(data, meta)
293	}
294
295	return r.recordCurrentSchemaVersion(data.State()), err
296}
297
298// Diff returns a diff of this resource.
299func (r *Resource) Diff(
300	s *terraform.InstanceState,
301	c *terraform.ResourceConfig,
302	meta interface{}) (*terraform.InstanceDiff, error) {
303
304	t := &ResourceTimeout{}
305	err := t.ConfigDecode(r, c)
306
307	if err != nil {
308		return nil, fmt.Errorf("[ERR] Error decoding timeout: %s", err)
309	}
310
311	instanceDiff, err := schemaMap(r.Schema).Diff(s, c, r.CustomizeDiff, meta, true)
312	if err != nil {
313		return instanceDiff, err
314	}
315
316	if instanceDiff != nil {
317		if err := t.DiffEncode(instanceDiff); err != nil {
318			log.Printf("[ERR] Error encoding timeout to instance diff: %s", err)
319		}
320	} else {
321		log.Printf("[DEBUG] Instance Diff is nil in Diff()")
322	}
323
324	return instanceDiff, err
325}
326
327func (r *Resource) simpleDiff(
328	s *terraform.InstanceState,
329	c *terraform.ResourceConfig,
330	meta interface{}) (*terraform.InstanceDiff, error) {
331
332	t := &ResourceTimeout{}
333	err := t.ConfigDecode(r, c)
334
335	if err != nil {
336		return nil, fmt.Errorf("[ERR] Error decoding timeout: %s", err)
337	}
338
339	instanceDiff, err := schemaMap(r.Schema).Diff(s, c, r.CustomizeDiff, meta, false)
340	if err != nil {
341		return instanceDiff, err
342	}
343
344	if instanceDiff == nil {
345		log.Printf("[DEBUG] Instance Diff is nil in SimpleDiff()")
346		return nil, err
347	}
348
349	// Make sure the old value is set in each of the instance diffs.
350	// This was done by the RequiresNew logic in the full legacy Diff.
351	for k, attr := range instanceDiff.Attributes {
352		if attr == nil {
353			continue
354		}
355		if s != nil {
356			attr.Old = s.Attributes[k]
357		}
358	}
359
360	if err := t.DiffEncode(instanceDiff); err != nil {
361		log.Printf("[ERR] Error encoding timeout to instance diff: %s", err)
362	}
363	return instanceDiff, err
364}
365
366// Validate validates the resource configuration against the schema.
367func (r *Resource) Validate(c *terraform.ResourceConfig) ([]string, []error) {
368	warns, errs := schemaMap(r.Schema).Validate(c)
369
370	if r.DeprecationMessage != "" {
371		warns = append(warns, r.DeprecationMessage)
372	}
373
374	return warns, errs
375}
376
377// ReadDataApply loads the data for a data source, given a diff that
378// describes the configuration arguments and desired computed attributes.
379func (r *Resource) ReadDataApply(
380	d *terraform.InstanceDiff,
381	meta interface{},
382) (*terraform.InstanceState, error) {
383	// Data sources are always built completely from scratch
384	// on each read, so the source state is always nil.
385	data, err := schemaMap(r.Schema).Data(nil, d)
386	if err != nil {
387		return nil, err
388	}
389
390	err = r.Read(data, meta)
391	state := data.State()
392	if state != nil && state.ID == "" {
393		// Data sources can set an ID if they want, but they aren't
394		// required to; we'll provide a placeholder if they don't,
395		// to preserve the invariant that all resources have non-empty
396		// ids.
397		state.ID = "-"
398	}
399
400	return r.recordCurrentSchemaVersion(state), err
401}
402
403// RefreshWithoutUpgrade reads the instance state, but does not call
404// MigrateState or the StateUpgraders, since those are now invoked in a
405// separate API call.
406// RefreshWithoutUpgrade is part of the new plugin shims.
407func (r *Resource) RefreshWithoutUpgrade(
408	s *terraform.InstanceState,
409	meta interface{}) (*terraform.InstanceState, error) {
410	// If the ID is already somehow blank, it doesn't exist
411	if s.ID == "" {
412		return nil, nil
413	}
414
415	rt := ResourceTimeout{}
416	if _, ok := s.Meta[TimeoutKey]; ok {
417		if err := rt.StateDecode(s); err != nil {
418			log.Printf("[ERR] Error decoding ResourceTimeout: %s", err)
419		}
420	}
421
422	if r.Exists != nil {
423		// Make a copy of data so that if it is modified it doesn't
424		// affect our Read later.
425		data, err := schemaMap(r.Schema).Data(s, nil)
426		data.timeouts = &rt
427
428		if err != nil {
429			return s, err
430		}
431
432		exists, err := r.Exists(data, meta)
433		if err != nil {
434			return s, err
435		}
436		if !exists {
437			return nil, nil
438		}
439	}
440
441	data, err := schemaMap(r.Schema).Data(s, nil)
442	data.timeouts = &rt
443	if err != nil {
444		return s, err
445	}
446
447	err = r.Read(data, meta)
448	state := data.State()
449	if state != nil && state.ID == "" {
450		state = nil
451	}
452
453	return r.recordCurrentSchemaVersion(state), err
454}
455
456// Refresh refreshes the state of the resource.
457func (r *Resource) Refresh(
458	s *terraform.InstanceState,
459	meta interface{}) (*terraform.InstanceState, error) {
460	// If the ID is already somehow blank, it doesn't exist
461	if s.ID == "" {
462		return nil, nil
463	}
464
465	rt := ResourceTimeout{}
466	if _, ok := s.Meta[TimeoutKey]; ok {
467		if err := rt.StateDecode(s); err != nil {
468			log.Printf("[ERR] Error decoding ResourceTimeout: %s", err)
469		}
470	}
471
472	if r.Exists != nil {
473		// Make a copy of data so that if it is modified it doesn't
474		// affect our Read later.
475		data, err := schemaMap(r.Schema).Data(s, nil)
476		data.timeouts = &rt
477
478		if err != nil {
479			return s, err
480		}
481
482		exists, err := r.Exists(data, meta)
483		if err != nil {
484			return s, err
485		}
486		if !exists {
487			return nil, nil
488		}
489	}
490
491	// there may be new StateUpgraders that need to be run
492	s, err := r.upgradeState(s, meta)
493	if err != nil {
494		return s, err
495	}
496
497	data, err := schemaMap(r.Schema).Data(s, nil)
498	data.timeouts = &rt
499	if err != nil {
500		return s, err
501	}
502
503	err = r.Read(data, meta)
504	state := data.State()
505	if state != nil && state.ID == "" {
506		state = nil
507	}
508
509	return r.recordCurrentSchemaVersion(state), err
510}
511
512func (r *Resource) upgradeState(s *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) {
513	var err error
514
515	needsMigration, stateSchemaVersion := r.checkSchemaVersion(s)
516	migrate := needsMigration && r.MigrateState != nil
517
518	if migrate {
519		s, err = r.MigrateState(stateSchemaVersion, s, meta)
520		if err != nil {
521			return s, err
522		}
523	}
524
525	if len(r.StateUpgraders) == 0 {
526		return s, nil
527	}
528
529	// If we ran MigrateState, then the stateSchemaVersion value is no longer
530	// correct. We can expect the first upgrade function to be the correct
531	// schema type version.
532	if migrate {
533		stateSchemaVersion = r.StateUpgraders[0].Version
534	}
535
536	schemaType := r.CoreConfigSchema().ImpliedType()
537	// find the expected type to convert the state
538	for _, upgrader := range r.StateUpgraders {
539		if stateSchemaVersion == upgrader.Version {
540			schemaType = upgrader.Type
541		}
542	}
543
544	// StateUpgraders only operate on the new JSON format state, so the state
545	// need to be converted.
546	stateVal, err := StateValueFromInstanceState(s, schemaType)
547	if err != nil {
548		return nil, err
549	}
550
551	jsonState, err := StateValueToJSONMap(stateVal, schemaType)
552	if err != nil {
553		return nil, err
554	}
555
556	for _, upgrader := range r.StateUpgraders {
557		if stateSchemaVersion != upgrader.Version {
558			continue
559		}
560
561		jsonState, err = upgrader.Upgrade(jsonState, meta)
562		if err != nil {
563			return nil, err
564		}
565		stateSchemaVersion++
566	}
567
568	// now we need to re-flatmap the new state
569	stateVal, err = JSONMapToStateValue(jsonState, r.CoreConfigSchema())
570	if err != nil {
571		return nil, err
572	}
573
574	return r.ShimInstanceStateFromValue(stateVal)
575}
576
577// InternalValidate should be called to validate the structure
578// of the resource.
579//
580// This should be called in a unit test for any resource to verify
581// before release that a resource is properly configured for use with
582// this library.
583//
584// Provider.InternalValidate() will automatically call this for all of
585// the resources it manages, so you don't need to call this manually if it
586// is part of a Provider.
587func (r *Resource) InternalValidate(topSchemaMap schemaMap, writable bool) error {
588	if r == nil {
589		return errors.New("resource is nil")
590	}
591
592	if !writable {
593		if r.Create != nil || r.Update != nil || r.Delete != nil {
594			return fmt.Errorf("must not implement Create, Update or Delete")
595		}
596
597		// CustomizeDiff cannot be defined for read-only resources
598		if r.CustomizeDiff != nil {
599			return fmt.Errorf("cannot implement CustomizeDiff")
600		}
601	}
602
603	tsm := topSchemaMap
604
605	if r.isTopLevel() && writable {
606		// All non-Computed attributes must be ForceNew if Update is not defined
607		if r.Update == nil {
608			nonForceNewAttrs := make([]string, 0)
609			for k, v := range r.Schema {
610				if !v.ForceNew && !v.Computed {
611					nonForceNewAttrs = append(nonForceNewAttrs, k)
612				}
613			}
614			if len(nonForceNewAttrs) > 0 {
615				return fmt.Errorf(
616					"No Update defined, must set ForceNew on: %#v", nonForceNewAttrs)
617			}
618		} else {
619			nonUpdateableAttrs := make([]string, 0)
620			for k, v := range r.Schema {
621				if v.ForceNew || v.Computed && !v.Optional {
622					nonUpdateableAttrs = append(nonUpdateableAttrs, k)
623				}
624			}
625			updateableAttrs := len(r.Schema) - len(nonUpdateableAttrs)
626			if updateableAttrs == 0 {
627				return fmt.Errorf(
628					"All fields are ForceNew or Computed w/out Optional, Update is superfluous")
629			}
630		}
631
632		tsm = schemaMap(r.Schema)
633
634		// Destroy, and Read are required
635		if r.Read == nil {
636			return fmt.Errorf("Read must be implemented")
637		}
638		if r.Delete == nil {
639			return fmt.Errorf("Delete must be implemented")
640		}
641
642		// If we have an importer, we need to verify the importer.
643		if r.Importer != nil {
644			if err := r.Importer.InternalValidate(); err != nil {
645				return err
646			}
647		}
648
649		for k, f := range tsm {
650			if isReservedResourceFieldName(k, f) {
651				return fmt.Errorf("%s is a reserved field name", k)
652			}
653		}
654	}
655
656	lastVersion := -1
657	for _, u := range r.StateUpgraders {
658		if lastVersion >= 0 && u.Version-lastVersion > 1 {
659			return fmt.Errorf("missing schema version between %d and %d", lastVersion, u.Version)
660		}
661
662		if u.Version >= r.SchemaVersion {
663			return fmt.Errorf("StateUpgrader version %d is >= current version %d", u.Version, r.SchemaVersion)
664		}
665
666		if !u.Type.IsObjectType() {
667			return fmt.Errorf("StateUpgrader %d type is not cty.Object", u.Version)
668		}
669
670		if u.Upgrade == nil {
671			return fmt.Errorf("StateUpgrader %d missing StateUpgradeFunc", u.Version)
672		}
673
674		lastVersion = u.Version
675	}
676
677	if lastVersion >= 0 && lastVersion != r.SchemaVersion-1 {
678		return fmt.Errorf("missing StateUpgrader between %d and %d", lastVersion, r.SchemaVersion)
679	}
680
681	// Data source
682	if r.isTopLevel() && !writable {
683		tsm = schemaMap(r.Schema)
684		for k, _ := range tsm {
685			if isReservedDataSourceFieldName(k) {
686				return fmt.Errorf("%s is a reserved field name", k)
687			}
688		}
689	}
690
691	return schemaMap(r.Schema).InternalValidate(tsm)
692}
693
694func isReservedDataSourceFieldName(name string) bool {
695	for _, reservedName := range config.ReservedDataSourceFields {
696		if name == reservedName {
697			return true
698		}
699	}
700	return false
701}
702
703func isReservedResourceFieldName(name string, s *Schema) bool {
704	// Allow phasing out "id"
705	// See https://github.com/terraform-providers/terraform-provider-aws/pull/1626#issuecomment-328881415
706	if name == "id" && (s.Deprecated != "" || s.Removed != "") {
707		return false
708	}
709
710	for _, reservedName := range config.ReservedResourceFields {
711		if name == reservedName {
712			return true
713		}
714	}
715	return false
716}
717
718// Data returns a ResourceData struct for this Resource. Each return value
719// is a separate copy and can be safely modified differently.
720//
721// The data returned from this function has no actual affect on the Resource
722// itself (including the state given to this function).
723//
724// This function is useful for unit tests and ResourceImporter functions.
725func (r *Resource) Data(s *terraform.InstanceState) *ResourceData {
726	result, err := schemaMap(r.Schema).Data(s, nil)
727	if err != nil {
728		// At the time of writing, this isn't possible (Data never returns
729		// non-nil errors). We panic to find this in the future if we have to.
730		// I don't see a reason for Data to ever return an error.
731		panic(err)
732	}
733
734	// load the Resource timeouts
735	result.timeouts = r.Timeouts
736	if result.timeouts == nil {
737		result.timeouts = &ResourceTimeout{}
738	}
739
740	// Set the schema version to latest by default
741	result.meta = map[string]interface{}{
742		"schema_version": strconv.Itoa(r.SchemaVersion),
743	}
744
745	return result
746}
747
748// TestResourceData Yields a ResourceData filled with this resource's schema for use in unit testing
749//
750// TODO: May be able to be removed with the above ResourceData function.
751func (r *Resource) TestResourceData() *ResourceData {
752	return &ResourceData{
753		schema: r.Schema,
754	}
755}
756
757// SchemasForFlatmapPath tries its best to find a sequence of schemas that
758// the given dot-delimited attribute path traverses through in the schema
759// of the receiving Resource.
760func (r *Resource) SchemasForFlatmapPath(path string) []*Schema {
761	return SchemasForFlatmapPath(path, r.Schema)
762}
763
764// Returns true if the resource is "top level" i.e. not a sub-resource.
765func (r *Resource) isTopLevel() bool {
766	// TODO: This is a heuristic; replace with a definitive attribute?
767	return (r.Create != nil || r.Read != nil)
768}
769
770// Determines if a given InstanceState needs to be migrated by checking the
771// stored version number with the current SchemaVersion
772func (r *Resource) checkSchemaVersion(is *terraform.InstanceState) (bool, int) {
773	// Get the raw interface{} value for the schema version. If it doesn't
774	// exist or is nil then set it to zero.
775	raw := is.Meta["schema_version"]
776	if raw == nil {
777		raw = "0"
778	}
779
780	// Try to convert it to a string. If it isn't a string then we pretend
781	// that it isn't set at all. It should never not be a string unless it
782	// was manually tampered with.
783	rawString, ok := raw.(string)
784	if !ok {
785		rawString = "0"
786	}
787
788	stateSchemaVersion, _ := strconv.Atoi(rawString)
789
790	// Don't run MigrateState if the version is handled by a StateUpgrader,
791	// since StateMigrateFuncs are not required to handle unknown versions
792	maxVersion := r.SchemaVersion
793	if len(r.StateUpgraders) > 0 {
794		maxVersion = r.StateUpgraders[0].Version
795	}
796
797	return stateSchemaVersion < maxVersion, stateSchemaVersion
798}
799
800func (r *Resource) recordCurrentSchemaVersion(
801	state *terraform.InstanceState) *terraform.InstanceState {
802	if state != nil && r.SchemaVersion > 0 {
803		if state.Meta == nil {
804			state.Meta = make(map[string]interface{})
805		}
806		state.Meta["schema_version"] = strconv.Itoa(r.SchemaVersion)
807	}
808	return state
809}
810
811// Noop is a convenience implementation of resource function which takes
812// no action and returns no error.
813func Noop(*ResourceData, interface{}) error {
814	return nil
815}
816
817// RemoveFromState is a convenience implementation of a resource function
818// which sets the resource ID to empty string (to remove it from state)
819// and returns no error.
820func RemoveFromState(d *ResourceData, _ interface{}) error {
821	d.SetId("")
822	return nil
823}
824