helper/diff: work with complex data types

This commit is contained in:
Mitchell Hashimoto 2014-07-01 16:06:06 -07:00
parent d2d6ef64aa
commit 37995e7ff8
2 changed files with 123 additions and 31 deletions

View File

@ -1,14 +1,43 @@
package diff package diff
import ( import (
"strings"
"github.com/hashicorp/terraform/flatmap"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
// AttrType is an enum that tells the ResourceBuilder what type of attribute
// an attribute is, affecting the overall diff output.
//
// The valid values are:
//
// * AttrTypeCreate - This attribute can only be set or updated on create.
// This means that if this attribute is changed, it will require a new
// resource to be created if it is already created.
//
// * AttrTypeUpdate - This attribute can be set at create time or updated
// in-place. Changing this attribute does not require a new resource.
//
type AttrType byte
const (
AttrTypeUnknown AttrType = iota
AttrTypeCreate
AttrTypeUpdate
)
// ResourceBuilder is a helper that knows about how a single resource // ResourceBuilder is a helper that knows about how a single resource
// changes and how those changes affect the diff. // changes and how those changes affect the diff.
type ResourceBuilder struct { type ResourceBuilder struct {
CreateComputedAttrs []string // Attrs are the mapping of attributes that can be set from the
RequiresNewAttrs []string // configuration, and the affect they have. See the documentation for
// AttrType for more info.
Attrs map[string]AttrType
// ComputedAttrs are the attributes that are computed at
// resource creation time.
ComputedAttrs []string
} }
// Diff returns the ResourceDiff for a resource given its state and // Diff returns the ResourceDiff for a resource given its state and
@ -18,45 +47,52 @@ func (b *ResourceBuilder) Diff(
c *terraform.ResourceConfig) (*terraform.ResourceDiff, error) { c *terraform.ResourceConfig) (*terraform.ResourceDiff, error) {
attrs := make(map[string]*terraform.ResourceAttrDiff) attrs := make(map[string]*terraform.ResourceAttrDiff)
requiresNewSet := make(map[string]struct{})
for _, k := range b.RequiresNewAttrs {
requiresNewSet[k] = struct{}{}
}
// We require a new resource if the ID is empty. Or, later, we set // We require a new resource if the ID is empty. Or, later, we set
// this to true if any configuration changed that triggers a new resource. // this to true if any configuration changed that triggers a new resource.
requiresNew := s.ID == "" requiresNew := s.ID == ""
// Go through the configuration and find the changed attributes // Flatten the raw and processed configuration
for k, v := range c.Raw { flatRaw := flatmap.Flatten(c.Raw)
newV := v.(string) flatConfig := flatmap.Flatten(c.Config)
for k, v := range flatRaw {
// Make sure this is an attribute that actually affects
// the diff in some way.
var attr AttrType
for ak, at := range b.Attrs {
if strings.HasPrefix(k, ak) {
attr = at
break
}
}
if attr == AttrTypeUnknown {
continue
}
// If this key is in the cleaned config, then use that value // If this key is in the cleaned config, then use that value
// because it'll have its variables properly interpolated // because it'll have its variables properly interpolated
if cleanV, ok := c.Config[k]; ok { if cleanV, ok := flatConfig[k]; ok {
newV = cleanV.(string) v = cleanV
} }
var oldV string oldV, ok := s.Attributes[k]
var ok bool
if oldV, ok = s.Attributes[k]; ok { // If there is an old value and they're the same, no change
// Old value exists! We check to see if there is a change if ok && oldV == v {
if oldV == newV { continue
continue
}
} }
// There has been a change. Record it // Record the change
attrs[k] = &terraform.ResourceAttrDiff{ attrs[k] = &terraform.ResourceAttrDiff{
Old: oldV, Old: oldV,
New: newV, New: v,
Type: terraform.DiffAttrInput,
} }
// If this requires a new resource, record that and flag our // If this requires a new resource, record that and flag our
// boolean. // boolean.
if _, ok := requiresNewSet[k]; ok { if attr == AttrTypeCreate {
attrs[k].RequiresNew = true attrs[k].RequiresNew = true
attrs[k].Type = terraform.DiffAttrInput
requiresNew = true requiresNew = true
} }
} }
@ -64,7 +100,7 @@ func (b *ResourceBuilder) Diff(
// If we require a new resource, then process all the attributes // If we require a new resource, then process all the attributes
// that will be changing due to the creation of the resource. // that will be changing due to the creation of the resource.
if requiresNew { if requiresNew {
for _, k := range b.CreateComputedAttrs { for _, k := range b.ComputedAttrs {
old := s.Attributes[k] old := s.Attributes[k]
attrs[k] = &terraform.ResourceAttrDiff{ attrs[k] = &terraform.ResourceAttrDiff{
Old: old, Old: old,

View File

@ -7,9 +7,51 @@ import (
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func TestResourceBuilder_complex(t *testing.T) {
rb := &ResourceBuilder{
Attrs: map[string]AttrType{
"listener": AttrTypeUpdate,
},
}
state := &terraform.ResourceState{
ID: "foo",
Attributes: map[string]string{
"ignore": "1",
"listener.#": "1",
"listener.0.port": "80",
},
}
c := testConfig(t, map[string]interface{}{
"listener": []interface{}{
map[interface{}]interface{}{
"port": 3000,
},
},
}, nil)
diff, err := rb.Diff(state, c)
if err != nil {
t.Fatalf("err: %s", err)
}
if diff == nil {
t.Fatal("should not be nil")
}
actual := testResourceDiffStr(diff)
expected := testRBComplexDiff
if actual != expected {
t.Fatalf("bad: %s", actual)
}
}
func TestResourceBuilder_new(t *testing.T) { func TestResourceBuilder_new(t *testing.T) {
rb := &ResourceBuilder{ rb := &ResourceBuilder{
CreateComputedAttrs: []string{"private_ip"}, Attrs: map[string]AttrType{
"foo": AttrTypeUpdate,
},
ComputedAttrs: []string{"private_ip"},
} }
state := &terraform.ResourceState{} state := &terraform.ResourceState{}
@ -35,8 +77,10 @@ func TestResourceBuilder_new(t *testing.T) {
func TestResourceBuilder_requiresNew(t *testing.T) { func TestResourceBuilder_requiresNew(t *testing.T) {
rb := &ResourceBuilder{ rb := &ResourceBuilder{
CreateComputedAttrs: []string{"private_ip"}, ComputedAttrs: []string{"private_ip"},
RequiresNewAttrs: []string{"ami"}, Attrs: map[string]AttrType{
"ami": AttrTypeCreate,
},
} }
state := &terraform.ResourceState{ state := &terraform.ResourceState{
@ -68,7 +112,7 @@ func TestResourceBuilder_requiresNew(t *testing.T) {
func TestResourceBuilder_same(t *testing.T) { func TestResourceBuilder_same(t *testing.T) {
rb := &ResourceBuilder{ rb := &ResourceBuilder{
CreateComputedAttrs: []string{"private_ip"}, ComputedAttrs: []string{"private_ip"},
} }
state := &terraform.ResourceState{ state := &terraform.ResourceState{
@ -92,7 +136,11 @@ func TestResourceBuilder_same(t *testing.T) {
} }
func TestResourceBuilder_unknown(t *testing.T) { func TestResourceBuilder_unknown(t *testing.T) {
rb := &ResourceBuilder{} rb := &ResourceBuilder{
Attrs: map[string]AttrType{
"foo": AttrTypeUpdate,
},
}
state := &terraform.ResourceState{} state := &terraform.ResourceState{}
@ -119,7 +167,11 @@ func TestResourceBuilder_unknown(t *testing.T) {
} }
func TestResourceBuilder_vars(t *testing.T) { func TestResourceBuilder_vars(t *testing.T) {
rb := &ResourceBuilder{} rb := &ResourceBuilder{
Attrs: map[string]AttrType{
"foo": AttrTypeUpdate,
},
}
state := &terraform.ResourceState{} state := &terraform.ResourceState{}
@ -144,6 +196,10 @@ func TestResourceBuilder_vars(t *testing.T) {
} }
} }
const testRBComplexDiff = `UPDATE
IN listener.0.port: "80" => "3000"
`
const testRBNewDiff = `UPDATE const testRBNewDiff = `UPDATE
IN foo: "" => "bar" IN foo: "" => "bar"
OUT private_ip: "" => "<computed>" OUT private_ip: "" => "<computed>"