helper/schema: diffing should use ResourceData for guidance

This commit is contained in:
Mitchell Hashimoto 2014-08-20 15:45:34 -07:00
parent 6eb6d19384
commit 5e975e47cf
5 changed files with 196 additions and 224 deletions

View File

@ -18,15 +18,18 @@ type getSource byte
const (
getSourceState getSource = iota
getSourceConfig
getSourceDiff
getSourceSet
)
// ResourceData is used to query and set the attributes of a resource.
type ResourceData struct {
schema map[string]*Schema
state *terraform.ResourceState
diff *terraform.ResourceDiff
schema map[string]*Schema
config *terraform.ResourceConfig
state *terraform.ResourceState
diff *terraform.ResourceDiff
diffing bool
setMap map[string]string
newState *terraform.ResourceState
@ -51,14 +54,7 @@ func (d *ResourceData) Get(key string) interface{} {
//
// If there is no change, then old and new will simply be the same.
func (d *ResourceData) GetChange(key string) (interface{}, interface{}) {
var parts []string
if key != "" {
parts = strings.Split(key, ".")
}
o := d.getObject("", parts, d.schema, getSourceState)
n := d.getObject("", parts, d.schema, getSourceDiff)
return o, n
return d.getChange(key, getSourceConfig, getSourceDiff)
}
// HasChange returns whether or not the given key has been changed.
@ -145,6 +141,28 @@ func (d *ResourceData) init() {
d.newState = &copyState
}
func (d *ResourceData) diffChange(k string) (interface{}, interface{}, bool) {
// Get the change between the state and the config.
o, n := d.getChange(k, getSourceState, getSourceConfig)
// Return the old, new, and whether there is a change
return o, n, !reflect.DeepEqual(o, n)
}
func (d *ResourceData) getChange(
key string,
oldLevel getSource,
newLevel getSource) (interface{}, interface{}) {
var parts []string
if key != "" {
parts = strings.Split(key, ".")
}
o := d.getObject("", parts, d.schema, oldLevel)
n := d.getObject("", parts, d.schema, newLevel)
return o, n
}
func (d *ResourceData) get(
k string,
parts []string,
@ -181,6 +199,15 @@ func (d *ResourceData) getMap(
}
}
if d.config != nil && source == getSourceConfig {
// For config, we always set the result to exactly what was requested
if m, ok := d.config.Get(k); ok {
result = m.(map[string]interface{})
} else {
result = nil
}
}
if d.diff != nil && source >= getSourceDiff {
for k, v := range d.diff.Attributes {
if !strings.HasPrefix(k, prefix) {
@ -303,6 +330,19 @@ func (d *ResourceData) getPrimitive(
result, resultSet = d.state.Attributes[k]
}
if d.config != nil && source == getSourceConfig {
// For config, we always return the exact value
if v, ok := d.config.Get(k); ok {
if err := mapstructure.WeakDecode(v, &result); err != nil {
panic(err)
}
} else {
result = ""
}
resultSet = true
}
if d.diff != nil && source >= getSourceDiff {
attrD, ok := d.diff.Attributes[k]
if ok && !attrD.NewComputed {

View File

@ -49,7 +49,12 @@ type Schema struct {
// TypeList, and represents what the element type is. If it is *Schema,
// the element type is just a simple value. If it is *Resource, the
// element type is a complex structure, potentially with its own lifecycle.
Elem interface{}
//
// Order defines a function to be called to order the elements in the
// list. See SchemaOrderFunc for more info. If Order is set, then any
// access of this list will result in the ordered list.
Elem interface{}
Order SchemaOrderFunc
// ComputedWhen is a set of queries on the configuration. Whenever any
// of these things is changed, it will require a recompute (this requires
@ -57,6 +62,10 @@ type Schema struct {
ComputedWhen []string
}
// SchemaOrderFunc is the function used to compare two elements in a list
// for ordering. It should return a boolean true if a is less than b.
type SchemaOrderFunc func(a, b interface{}) bool
func (s *Schema) finalizeDiff(
d *terraform.ResourceAttrDiff) *terraform.ResourceAttrDiff {
if d == nil {
@ -108,8 +117,15 @@ func (m schemaMap) Diff(
result := new(terraform.ResourceDiff)
result.Attributes = make(map[string]*terraform.ResourceAttrDiff)
d := &ResourceData{
schema: m,
state: s,
config: c,
diffing: true,
}
for k, schema := range m {
err := m.diff(k, schema, result, s, c)
err := m.diff(k, schema, result, d)
if err != nil {
return nil, err
}
@ -187,8 +203,7 @@ func (m schemaMap) diff(
k string,
schema *Schema,
diff *terraform.ResourceDiff,
s *terraform.ResourceState,
c *terraform.ResourceConfig) error {
d *ResourceData) error {
var err error
switch schema.Type {
case TypeBool:
@ -196,11 +211,11 @@ func (m schemaMap) diff(
case TypeInt:
fallthrough
case TypeString:
err = m.diffString(k, schema, diff, s, c)
err = m.diffString(k, schema, diff, d)
case TypeList:
err = m.diffList(k, schema, diff, s, c)
err = m.diffList(k, schema, diff, d)
case TypeMap:
err = m.diffMap(k, schema, diff, s, c)
err = m.diffMap(k, schema, diff, d)
default:
err = fmt.Errorf("%s: unknown type %s", k, schema.Type)
}
@ -212,40 +227,14 @@ func (m schemaMap) diffList(
k string,
schema *Schema,
diff *terraform.ResourceDiff,
s *terraform.ResourceState,
c *terraform.ResourceConfig) error {
var vs []interface{}
v, ok := c.Get(k)
if ok {
// We have to use reflection to build the []interface{} list
rawV := reflect.ValueOf(v)
if rawV.Kind() != reflect.Slice {
return fmt.Errorf("%s: must be a list", k)
}
vs = make([]interface{}, rawV.Len())
for i, _ := range vs {
vs[i] = rawV.Index(i).Interface()
}
}
// If this field is required, then it must also be non-empty
if len(vs) == 0 && schema.Required {
return fmt.Errorf("%s: required field is not set", k)
}
d *ResourceData) error {
o, n, _ := d.diffChange(k)
os := o.([]interface{})
vs := n.([]interface{})
// Get the counts
var oldLen, newLen int
if s != nil {
if v, ok := s.Attributes[k+".#"]; ok {
old64, err := strconv.ParseInt(v, 0, 0)
if err != nil {
return err
}
oldLen = int(old64)
}
}
newLen = len(vs)
oldLen := len(os)
newLen := len(vs)
// If the counts are not the same, then record that diff
changed := oldLen != newLen
@ -287,7 +276,7 @@ func (m schemaMap) diffList(
// just diff each.
for i := 0; i < maxLen; i++ {
subK := fmt.Sprintf("%s.%d", k, i)
err := m.diff(subK, &t2, diff, s, c)
err := m.diff(subK, &t2, diff, d)
if err != nil {
return err
}
@ -297,7 +286,7 @@ func (m schemaMap) diffList(
for i := 0; i < maxLen; i++ {
for k2, schema := range t.Schema {
subK := fmt.Sprintf("%s.%d.%s", k, i, k2)
err := m.diff(subK, schema, diff, s, c)
err := m.diff(subK, schema, diff, d)
if err != nil {
return err
}
@ -314,31 +303,18 @@ func (m schemaMap) diffMap(
k string,
schema *Schema,
diff *terraform.ResourceDiff,
s *terraform.ResourceState,
c *terraform.ResourceConfig) error {
d *ResourceData) error {
//elemSchema := &Schema{Type: TypeString}
prefix := k + "."
// First get all the values from the state
stateMap := make(map[string]string)
if s != nil {
for sk, sv := range s.Attributes {
if !strings.HasPrefix(sk, prefix) {
continue
}
stateMap[sk[len(prefix):]] = sv
}
var stateMap, configMap map[string]string
o, n, _ := d.diffChange(k)
if err := mapstructure.WeakDecode(o, &stateMap); err != nil {
return fmt.Errorf("%s: %s", k, err)
}
// Then get all the values from the configuration
configMap := make(map[string]string)
if c != nil {
if raw, ok := c.Get(k); ok {
for k, v := range raw.(map[string]interface{}) {
configMap[k] = v.(string)
}
}
if err := mapstructure.WeakDecode(n, &configMap); err != nil {
return fmt.Errorf("%s: %s", k, err)
}
// Now we compare, preferring values from the config map
@ -369,66 +345,27 @@ func (m schemaMap) diffString(
k string,
schema *Schema,
diff *terraform.ResourceDiff,
s *terraform.ResourceState,
c *terraform.ResourceConfig) error {
var old, n string
if s != nil {
old = s.Attributes[k]
d *ResourceData) error {
var os, ns string
o, n, _ := d.diffChange(k)
if err := mapstructure.WeakDecode(o, &os); err != nil {
return fmt.Errorf("%s: %s", k, err)
}
v, ok := c.Get(k)
if !ok {
// We don't have a value, if it is required then it is an error
if schema.Required {
return fmt.Errorf("%s: required field not set", k)
}
// If we don't have an old value, just return
if old == "" && !schema.Computed {
return nil
}
} else {
if err := mapstructure.WeakDecode(v, &n); err != nil {
return fmt.Errorf("%s: %s", k, err)
}
if old == n {
// They're the same value
return nil
}
}
diff.Attributes[k] = schema.finalizeDiff(&terraform.ResourceAttrDiff{
Old: old,
New: n,
})
return nil
}
func (m schemaMap) diffPrimitive(
k string,
nraw interface{},
schema *Schema,
diff *terraform.ResourceDiff,
s *terraform.ResourceState) error {
var old, n string
if s != nil {
old = s.Attributes[k]
}
if err := mapstructure.WeakDecode(nraw, &n); err != nil {
if err := mapstructure.WeakDecode(n, &ns); err != nil {
return fmt.Errorf("%s: %s", k, err)
}
if old == n {
// They're the same value
return nil
if os == ns {
// They're the same value, return no diff as long as we're not
// computing a new value.
if os != "" || !schema.Computed {
return nil
}
}
diff.Attributes[k] = schema.finalizeDiff(&terraform.ResourceAttrDiff{
Old: old,
New: n,
Old: os,
New: ns,
})
return nil

View File

@ -0,0 +1,20 @@
package schema
// listSort implements sort.Interface to sort a list of []interface according
// to a schema.
type listSort struct {
List []interface{}
Schema *Schema
}
func (s *listSort) Len() int {
return len(s.List)
}
func (s *listSort) Less(i, j int) bool {
return s.Schema.Order(s.List[i], s.List[j])
}
func (s *listSort) Swap(i, j int) {
s.List[i], s.List[j] = s.List[j], s.List[i]
}

View File

@ -0,0 +1,29 @@
package schema
import (
"reflect"
"sort"
"testing"
)
func TestListSort_impl(t *testing.T) {
var _ sort.Interface = new(listSort)
}
func TestListSort(t *testing.T) {
s := &listSort{
List: []interface{}{5, 2, 1},
Schema: &Schema{
Order: func(a, b interface{}) bool {
return a.(int) < b.(int)
},
},
}
sort.Sort(s)
expected := []interface{}{1, 2, 5}
if !reflect.DeepEqual(s.List, expected) {
t.Fatalf("bad: %#v", s.List)
}
}

View File

@ -76,23 +76,6 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Required: true,
},
},
State: nil,
Config: map[string]interface{}{},
Diff: nil,
Err: true,
},
/*
* Int decode
*/
@ -336,6 +319,50 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
/*
{
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeList,
Required: true,
Elem: &Schema{Type: TypeInt},
Order: func(a, b interface{}) bool {
return a.(int) < b.(int)
},
},
},
State: nil,
Config: map[string]interface{}{
"ports": []interface{}{5, 2, 1},
},
Diff: &terraform.ResourceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "3",
},
"ports.0": &terraform.ResourceAttrDiff{
Old: "",
New: "1",
},
"ports.1": &terraform.ResourceAttrDiff{
Old: "",
New: "2",
},
"ports.2": &terraform.ResourceAttrDiff{
Old: "",
New: "5",
},
},
},
Err: false,
},
*/
/*
* List of structure decode
*/
@ -382,87 +409,6 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
{
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeList,
Required: true,
Elem: &Resource{
Schema: map[string]*Schema{
"from": &Schema{
Type: TypeInt,
Required: true,
},
},
},
},
},
State: nil,
Config: map[string]interface{}{},
Diff: nil,
Err: true,
},
{
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeList,
Required: true,
Elem: &Resource{
Schema: map[string]*Schema{
"from": &Schema{
Type: TypeInt,
Required: true,
},
},
},
},
},
State: nil,
Config: map[string]interface{}{
"ingress": []interface{}{},
},
Diff: nil,
Err: true,
},
{
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeList,
Required: true,
Elem: &Resource{
Schema: map[string]*Schema{
"from": &Schema{
Type: TypeInt,
Required: true,
},
},
},
},
},
State: nil,
Config: map[string]interface{}{
"ingress": []interface{}{
map[string]interface{}{},
},
},
Diff: nil,
Err: true,
},
/*
* ComputedWhen
*/
@ -666,7 +612,7 @@ func TestSchemaMap_Diff(t *testing.T) {
d, err := schemaMap(tc.Schema).Diff(
tc.State, terraform.NewResourceConfig(c))
if (err != nil) != tc.Err {
t.Fatalf("err: %s", err)
t.Fatalf("#%d err: %s", i, err)
}
if !reflect.DeepEqual(tc.Diff, d) {