"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/terraform"
+ "github.com/zclconf/go-cty/cty"
)
// Resource represents a thing in Terraform that has a set of configurable
// their Versioning at any integer >= 1
SchemaVersion int
+ // MigrateState is deprecated and any new changes to a resource's schema
+ // should be handled by StateUpgraders. Existing MigrateState implementations
+ // should remain for compatibility with existing state. MigrateState will
+ // still be called if the stored SchemaVersion is less than the
+ // first version of the StateUpgraders.
+ //
// MigrateState is responsible for updating an InstanceState with an old
// version to the format expected by the current version of the Schema.
//
// needs to make any remote API calls.
MigrateState StateMigrateFunc
+ // StateUpgraders contains the functions responsible for upgrading an
+ // existing state with an old schema version to a newer schema. It is
+ // called specifically by Terraform when the stored schema version is less
+ // than the current SchemaVersion of the Resource.
+ //
+ // StateUpgraders map specific schema versions to a StateUpgrader
+ // function. The registered versions are expected to be ordered,
+ // consecutive values. The initial value may be greater than 0 to account
+ // for legacy schemas that weren't recorded and can be handled by
+ // MigrateState.
+ StateUpgraders []StateUpgrader
+
// The functions below are the CRUD operations for this resource.
//
// The only optional operation is Update. If Update is not implemented,
Timeouts *ResourceTimeout
}
+// ShimInstanceStateFromValue converts a cty.Value to a
+// terraform.InstanceState.
+func (r *Resource) ShimInstanceStateFromValue(state cty.Value) (*terraform.InstanceState, error) {
+ // Get the raw shimmed value. While this is correct, the set hashes don't
+ // match those from the Schema.
+ s := terraform.NewInstanceStateShimmedFromValue(state, r.SchemaVersion)
+
+ // We now rebuild the state through the ResourceData, so that the set indexes
+ // match what helper/schema expects.
+ data, err := schemaMap(r.Schema).Data(s, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ s = data.State()
+ if s == nil {
+ s = &terraform.InstanceState{}
+ }
+ return s, nil
+}
+
// See Resource documentation.
type CreateFunc func(*ResourceData, interface{}) error
type StateMigrateFunc func(
int, *terraform.InstanceState, interface{}) (*terraform.InstanceState, error)
+type StateUpgrader struct {
+ // Version is the version schema that this Upgrader will handle, converting
+ // it to Version+1.
+ Version int
+
+ // Type describes the schema that this function can upgrade. Type is
+ // required to decode the schema if the state was stored in a legacy
+ // flatmap format.
+ Type cty.Type
+
+ // Upgrade takes the JSON encoded state and the provider meta value, and
+ // upgrades the state one single schema version. The provided state is
+ // deocded into the default json types using a map[string]interface{}. It
+ // is up to the StateUpgradeFunc to ensure that the returned value can be
+ // encoded using the new schema.
+ Upgrade StateUpgradeFunc
+}
+
+// See StateUpgrader
+type StateUpgradeFunc func(rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error)
+
// See Resource documentation.
type CustomizeDiffFunc func(*ResourceDiff, interface{}) error
return nil, fmt.Errorf("[ERR] Error decoding timeout: %s", err)
}
- instanceDiff, err := schemaMap(r.Schema).Diff(s, c, r.CustomizeDiff, meta)
+ instanceDiff, err := schemaMap(r.Schema).Diff(s, c, r.CustomizeDiff, meta, true)
if err != nil {
return instanceDiff, err
}
return instanceDiff, err
}
+func (r *Resource) simpleDiff(
+ s *terraform.InstanceState,
+ c *terraform.ResourceConfig,
+ meta interface{}) (*terraform.InstanceDiff, error) {
+
+ t := &ResourceTimeout{}
+ err := t.ConfigDecode(r, c)
+
+ if err != nil {
+ return nil, fmt.Errorf("[ERR] Error decoding timeout: %s", err)
+ }
+
+ instanceDiff, err := schemaMap(r.Schema).Diff(s, c, r.CustomizeDiff, meta, false)
+ if err != nil {
+ return instanceDiff, err
+ }
+
+ if instanceDiff == nil {
+ log.Printf("[DEBUG] Instance Diff is nil in SimpleDiff()")
+ return nil, err
+ }
+
+ // Make sure the old value is set in each of the instance diffs.
+ // This was done by the RequiresNew logic in the full legacy Diff.
+ for k, attr := range instanceDiff.Attributes {
+ if attr == nil {
+ continue
+ }
+ if s != nil {
+ attr.Old = s.Attributes[k]
+ }
+ }
+
+ if err := t.DiffEncode(instanceDiff); err != nil {
+ log.Printf("[ERR] Error encoding timeout to instance diff: %s", err)
+ }
+ return instanceDiff, err
+}
+
// Validate validates the resource configuration against the schema.
func (r *Resource) Validate(c *terraform.ResourceConfig) ([]string, []error) {
warns, errs := schemaMap(r.Schema).Validate(c)
return r.recordCurrentSchemaVersion(state), err
}
-// Refresh refreshes the state of the resource.
-func (r *Resource) Refresh(
+// RefreshWithoutUpgrade reads the instance state, but does not call
+// MigrateState or the StateUpgraders, since those are now invoked in a
+// separate API call.
+// RefreshWithoutUpgrade is part of the new plugin shims.
+func (r *Resource) RefreshWithoutUpgrade(
s *terraform.InstanceState,
meta interface{}) (*terraform.InstanceState, error) {
// If the ID is already somehow blank, it doesn't exist
}
}
- needsMigration, stateSchemaVersion := r.checkSchemaVersion(s)
- if needsMigration && r.MigrateState != nil {
- s, err := r.MigrateState(stateSchemaVersion, s, meta)
+ data, err := schemaMap(r.Schema).Data(s, nil)
+ data.timeouts = &rt
+ if err != nil {
+ return s, err
+ }
+
+ err = r.Read(data, meta)
+ state := data.State()
+ if state != nil && state.ID == "" {
+ state = nil
+ }
+
+ return r.recordCurrentSchemaVersion(state), err
+}
+
+// Refresh refreshes the state of the resource.
+func (r *Resource) Refresh(
+ s *terraform.InstanceState,
+ meta interface{}) (*terraform.InstanceState, error) {
+ // If the ID is already somehow blank, it doesn't exist
+ if s.ID == "" {
+ return nil, nil
+ }
+
+ rt := ResourceTimeout{}
+ if _, ok := s.Meta[TimeoutKey]; ok {
+ if err := rt.StateDecode(s); err != nil {
+ log.Printf("[ERR] Error decoding ResourceTimeout: %s", err)
+ }
+ }
+
+ if r.Exists != nil {
+ // Make a copy of data so that if it is modified it doesn't
+ // affect our Read later.
+ data, err := schemaMap(r.Schema).Data(s, nil)
+ data.timeouts = &rt
+
if err != nil {
return s, err
}
+
+ exists, err := r.Exists(data, meta)
+ if err != nil {
+ return s, err
+ }
+ if !exists {
+ return nil, nil
+ }
+ }
+
+ // there may be new StateUpgraders that need to be run
+ s, err := r.upgradeState(s, meta)
+ if err != nil {
+ return s, err
}
data, err := schemaMap(r.Schema).Data(s, nil)
return r.recordCurrentSchemaVersion(state), err
}
+func (r *Resource) upgradeState(s *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) {
+ var err error
+
+ needsMigration, stateSchemaVersion := r.checkSchemaVersion(s)
+ migrate := needsMigration && r.MigrateState != nil
+
+ if migrate {
+ s, err = r.MigrateState(stateSchemaVersion, s, meta)
+ if err != nil {
+ return s, err
+ }
+ }
+
+ if len(r.StateUpgraders) == 0 {
+ return s, nil
+ }
+
+ // If we ran MigrateState, then the stateSchemaVersion value is no longer
+ // correct. We can expect the first upgrade function to be the correct
+ // schema type version.
+ if migrate {
+ stateSchemaVersion = r.StateUpgraders[0].Version
+ }
+
+ schemaType := r.CoreConfigSchema().ImpliedType()
+ // find the expected type to convert the state
+ for _, upgrader := range r.StateUpgraders {
+ if stateSchemaVersion == upgrader.Version {
+ schemaType = upgrader.Type
+ }
+ }
+
+ // StateUpgraders only operate on the new JSON format state, so the state
+ // need to be converted.
+ stateVal, err := StateValueFromInstanceState(s, schemaType)
+ if err != nil {
+ return nil, err
+ }
+
+ jsonState, err := StateValueToJSONMap(stateVal, schemaType)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, upgrader := range r.StateUpgraders {
+ if stateSchemaVersion != upgrader.Version {
+ continue
+ }
+
+ jsonState, err = upgrader.Upgrade(jsonState, meta)
+ if err != nil {
+ return nil, err
+ }
+ stateSchemaVersion++
+ }
+
+ // now we need to re-flatmap the new state
+ stateVal, err = JSONMapToStateValue(jsonState, r.CoreConfigSchema())
+ if err != nil {
+ return nil, err
+ }
+
+ return r.ShimInstanceStateFromValue(stateVal)
+}
+
// InternalValidate should be called to validate the structure
// of the resource.
//
}
}
+ lastVersion := -1
+ for _, u := range r.StateUpgraders {
+ if lastVersion >= 0 && u.Version-lastVersion > 1 {
+ return fmt.Errorf("missing schema version between %d and %d", lastVersion, u.Version)
+ }
+
+ if u.Version >= r.SchemaVersion {
+ return fmt.Errorf("StateUpgrader version %d is >= current version %d", u.Version, r.SchemaVersion)
+ }
+
+ if !u.Type.IsObjectType() {
+ return fmt.Errorf("StateUpgrader %d type is not cty.Object", u.Version)
+ }
+
+ if u.Upgrade == nil {
+ return fmt.Errorf("StateUpgrader %d missing StateUpgradeFunc", u.Version)
+ }
+
+ lastVersion = u.Version
+ }
+
+ if lastVersion >= 0 && lastVersion != r.SchemaVersion-1 {
+ return fmt.Errorf("missing StateUpgrader between %d and %d", lastVersion, r.SchemaVersion)
+ }
+
// Data source
if r.isTopLevel() && !writable {
tsm = schemaMap(r.Schema)
}
}
+// SchemasForFlatmapPath tries its best to find a sequence of schemas that
+// the given dot-delimited attribute path traverses through in the schema
+// of the receiving Resource.
+func (r *Resource) SchemasForFlatmapPath(path string) []*Schema {
+ return SchemasForFlatmapPath(path, r.Schema)
+}
+
// Returns true if the resource is "top level" i.e. not a sub-resource.
func (r *Resource) isTopLevel() bool {
// TODO: This is a heuristic; replace with a definitive attribute?
}
stateSchemaVersion, _ := strconv.Atoi(rawString)
- return stateSchemaVersion < r.SchemaVersion, stateSchemaVersion
+
+ // Don't run MigrateState if the version is handled by a StateUpgrader,
+ // since StateMigrateFuncs are not required to handle unknown versions
+ maxVersion := r.SchemaVersion
+ if len(r.StateUpgraders) > 0 {
+ maxVersion = r.StateUpgraders[0].Version
+ }
+
+ return stateSchemaVersion < maxVersion, stateSchemaVersion
}
func (r *Resource) recordCurrentSchemaVersion(