implement UpgradeState for schema.Resource
This is the provider-side UpgradeState implementation for a particular resource. This new function will be called to upgrade a saved state with an old schema version to the current schema. UpgradeState also requires a record of the last schema and version that could have been stored as a flatmapped state. If the stored state is in the legacy flatmap format, this will allow the provider to properly decode the flatmapped state into the expected structure for the new json encoded state. If the stored state's version is below that of the LegacySchema.Version value, it will first be processed by the legacy MigrateState function.
This commit is contained in:
parent
bcc8be7400
commit
9eef5e3f91
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
"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
|
||||
|
@ -44,6 +45,21 @@ type Resource struct {
|
|||
// their Versioning at any integer >= 1
|
||||
SchemaVersion int
|
||||
|
||||
// LegacySchema is a record of the last schema version and type that
|
||||
// existed before the addition of an UpgradeState function.
|
||||
//
|
||||
// This allows the resource schema to continue to evolve, while providing a
|
||||
// record of how to decode a legacy state to be upgraded.
|
||||
//
|
||||
// LegacySchema is required when implementing UpgradeState.
|
||||
LegacySchema LegacySchemaVersion
|
||||
|
||||
// MigrateState is deprecated and any new changes to a resource's schema
|
||||
// should be handled by UpgradeState. Existing MigrateState implementations
|
||||
// should remain for compatibility with existing state. MigrateState will
|
||||
// still be called if the stored SchemaVersion is lower than the
|
||||
// LegacySchema.Version value.
|
||||
//
|
||||
// MigrateState is responsible for updating an InstanceState with an old
|
||||
// version to the format expected by the current version of the Schema.
|
||||
//
|
||||
|
@ -56,6 +72,17 @@ type Resource struct {
|
|||
// needs to make any remote API calls.
|
||||
MigrateState StateMigrateFunc
|
||||
|
||||
// UpgradeState is responsible for upgrading an existing state with an old
|
||||
// schema version to the current schema. It is called specifically by
|
||||
// Terraform when the stored schema version is less than the current
|
||||
// SchemaVersion of the Resource.
|
||||
//
|
||||
// StateUpgradeFunc takes the schema version, the state decoded using the
|
||||
// default json types in a map[string]interface{}, and the provider meta
|
||||
// value. The returned map value should encode into the proper format json
|
||||
// to match the current provider schema.
|
||||
UpgradeState StateUpgradeFunc
|
||||
|
||||
// The functions below are the CRUD operations for this resource.
|
||||
//
|
||||
// The only optional operation is Update. If Update is not implemented,
|
||||
|
@ -136,6 +163,11 @@ type Resource struct {
|
|||
Timeouts *ResourceTimeout
|
||||
}
|
||||
|
||||
type LegacySchemaVersion struct {
|
||||
Version int
|
||||
Type cty.Type
|
||||
}
|
||||
|
||||
// See Resource documentation.
|
||||
type CreateFunc func(*ResourceData, interface{}) error
|
||||
|
||||
|
@ -155,6 +187,9 @@ type ExistsFunc func(*ResourceData, interface{}) (bool, error)
|
|||
type StateMigrateFunc func(
|
||||
int, *terraform.InstanceState, interface{}) (*terraform.InstanceState, error)
|
||||
|
||||
// See Resource documentation.
|
||||
type StateUpgradeFunc func(int, map[string]interface{}, interface{}) (map[string]interface{}, error)
|
||||
|
||||
// See Resource documentation.
|
||||
type CustomizeDiffFunc func(*ResourceDiff, interface{}) error
|
||||
|
||||
|
@ -437,6 +472,14 @@ func (r *Resource) InternalValidate(topSchemaMap schemaMap, writable bool) error
|
|||
}
|
||||
}
|
||||
|
||||
if r.LegacySchema.Version >= r.SchemaVersion {
|
||||
return errors.New("LegacySchema.Version cannot be >= SchemaVersion")
|
||||
}
|
||||
|
||||
if r.UpgradeState != nil && !r.LegacySchema.Type.IsObjectType() {
|
||||
return fmt.Errorf("LegacySchema.Type requires a cty.Object, got: %#v", r.LegacySchema.Type)
|
||||
}
|
||||
|
||||
// Data source
|
||||
if r.isTopLevel() && !writable {
|
||||
tsm = schemaMap(r.Schema)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package schema
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
@ -8,7 +9,11 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/hashicorp/terraform/config"
|
||||
"github.com/hashicorp/terraform/config/hcl2shim"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
ctyjson "github.com/zclconf/go-cty/cty/json"
|
||||
)
|
||||
|
||||
func TestResourceApply_create(t *testing.T) {
|
||||
|
@ -1368,3 +1373,111 @@ func TestResourceData_timeouts(t *testing.T) {
|
|||
t.Fatalf("incorrect ResourceData timeouts: %#v\n", *data.timeouts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResource_UpgradeState(t *testing.T) {
|
||||
// Schema v2 it deals only in newfoo, which tracks foo as an int
|
||||
r := &Resource{
|
||||
SchemaVersion: 2,
|
||||
Schema: map[string]*Schema{
|
||||
"newfoo": &Schema{
|
||||
Type: TypeInt,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
r.LegacySchema.Version = 1
|
||||
r.LegacySchema.Type = cty.Object(map[string]cty.Type{
|
||||
"id": cty.String,
|
||||
"oldfoo": cty.Number,
|
||||
})
|
||||
|
||||
r.UpgradeState = func(
|
||||
v int,
|
||||
m map[string]interface{},
|
||||
meta interface{}) (map[string]interface{}, error) {
|
||||
|
||||
oldfoo, ok := m["oldfoo"].(float64)
|
||||
if !ok {
|
||||
t.Fatalf("expected 1.2, got %#v", m["oldfoo"])
|
||||
}
|
||||
m["newfoo"] = int(oldfoo * 10)
|
||||
delete(m, "oldfoo")
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
oldStateAttrs := map[string]string{
|
||||
"id": "bar",
|
||||
"oldfoo": "1.2",
|
||||
}
|
||||
|
||||
// convert the legacy flatmap state to the json equivalent
|
||||
val, err := hcl2shim.HCL2ValueFromFlatmap(oldStateAttrs, r.LegacySchema.Type)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
js, err := ctyjson.Marshal(val, r.LegacySchema.Type)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// unmarshal the state using the json default types
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal(js, &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
actual, err := r.UpgradeState(2, m, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
expected := map[string]interface{}{
|
||||
"id": "bar",
|
||||
"newfoo": 12,
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expected, actual) {
|
||||
t.Fatalf("expected: %#v\ngot: %#v\n", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResource_ValidateUpgradeState(t *testing.T) {
|
||||
r := &Resource{
|
||||
SchemaVersion: 2,
|
||||
Schema: map[string]*Schema{
|
||||
"newfoo": &Schema{
|
||||
Type: TypeInt,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := r.InternalValidate(nil, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r.LegacySchema.Version = 2
|
||||
if err := r.InternalValidate(nil, true); err == nil {
|
||||
t.Fatal("LegacySchema.Version cannot be >= SchemaVersion")
|
||||
}
|
||||
|
||||
r.LegacySchema.Version = 1
|
||||
|
||||
r.UpgradeState = func(v int, m map[string]interface{}, _ interface{}) (map[string]interface{}, error) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
if err := r.InternalValidate(nil, true); err == nil {
|
||||
t.Fatal("UpgradeState requires LegacySchema.Type")
|
||||
}
|
||||
|
||||
r.LegacySchema.Type = cty.Object(map[string]cty.Type{
|
||||
"id": cty.String,
|
||||
})
|
||||
|
||||
if err := r.InternalValidate(nil, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue