Merge #14887: Custom diff logic for providers

This commit is contained in:
Martin Atkins 2017-11-01 15:56:54 -07:00 committed by GitHub
commit 4787428cad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 2112 additions and 33 deletions

View File

@ -17,8 +17,9 @@ func Provider() terraform.ResourceProvider {
}, },
}, },
ResourcesMap: map[string]*schema.Resource{ ResourcesMap: map[string]*schema.Resource{
"test_resource": testResource(), "test_resource": testResource(),
"test_resource_gh12183": testResourceGH12183(), "test_resource_gh12183": testResourceGH12183(),
"test_resource_with_custom_diff": testResourceCustomDiff(),
}, },
DataSourcesMap: map[string]*schema.Resource{ DataSourcesMap: map[string]*schema.Resource{
"test_data_source": testDataSource(), "test_data_source": testDataSource(),

View File

@ -0,0 +1,154 @@
package test
import (
"fmt"
"github.com/hashicorp/terraform/helper/schema"
)
func testResourceCustomDiff() *schema.Resource {
return &schema.Resource{
Create: testResourceCustomDiffCreate,
Read: testResourceCustomDiffRead,
CustomizeDiff: testResourceCustomDiffCustomizeDiff,
Update: testResourceCustomDiffUpdate,
Delete: testResourceCustomDiffDelete,
Schema: map[string]*schema.Schema{
"required": {
Type: schema.TypeString,
Required: true,
},
"computed": {
Type: schema.TypeInt,
Computed: true,
},
"index": {
Type: schema.TypeInt,
Computed: true,
},
"veto": {
Type: schema.TypeBool,
Optional: true,
},
"list": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
},
}
}
type listDiffCases struct {
Type string
Value string
}
func testListDiffCases(index int) []listDiffCases {
switch index {
case 0:
return []listDiffCases{
{
Type: "add",
Value: "dc1",
},
}
case 1:
return []listDiffCases{
{
Type: "remove",
Value: "dc1",
},
{
Type: "add",
Value: "dc2",
},
{
Type: "add",
Value: "dc3",
},
}
}
return nil
}
func testListDiffCasesReadResult(index int) []interface{} {
switch index {
case 1:
return []interface{}{"dc1"}
default:
return []interface{}{"dc2", "dc3"}
}
}
func testResourceCustomDiffCreate(d *schema.ResourceData, meta interface{}) error {
d.SetId("testId")
// Required must make it through to Create
if _, ok := d.GetOk("required"); !ok {
return fmt.Errorf("missing attribute 'required', but it's required")
}
_, new := d.GetChange("computed")
expected := new.(int) - 1
actual := d.Get("index").(int)
if expected != actual {
return fmt.Errorf("expected computed to be 1 ahead of index, got computed: %d, index: %d", expected, actual)
}
d.Set("index", new)
return testResourceCustomDiffRead(d, meta)
}
func testResourceCustomDiffRead(d *schema.ResourceData, meta interface{}) error {
if err := d.Set("list", testListDiffCasesReadResult(d.Get("index").(int))); err != nil {
return err
}
return nil
}
func testResourceCustomDiffCustomizeDiff(d *schema.ResourceDiff, meta interface{}) error {
if d.Get("veto").(bool) == true {
return fmt.Errorf("veto is true, diff vetoed")
}
// Note that this gets put into state after the update, regardless of whether
// or not anything is acted upon in the diff.
d.SetNew("computed", d.Get("computed").(int)+1)
// This tests a diffed list, based off of the value of index
dcs := testListDiffCases(d.Get("index").(int))
s := d.Get("list").([]interface{})
for _, dc := range dcs {
switch dc.Type {
case "add":
s = append(s, dc.Value)
case "remove":
for i := range s {
if s[i].(string) == dc.Value {
copy(s[i:], s[i+1:])
s = s[:len(s)-1]
break
}
}
}
}
d.SetNew("list", s)
return nil
}
func testResourceCustomDiffUpdate(d *schema.ResourceData, meta interface{}) error {
_, new := d.GetChange("computed")
expected := new.(int) - 1
actual := d.Get("index").(int)
if expected != actual {
return fmt.Errorf("expected computed to be 1 ahead of index, got computed: %d, index: %d", expected, actual)
}
d.Set("index", new)
return testResourceCustomDiffRead(d, meta)
}
func testResourceCustomDiffDelete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}

View File

@ -0,0 +1,53 @@
package test
import (
"fmt"
"regexp"
"testing"
"github.com/hashicorp/terraform/helper/resource"
)
// TestResourceWithCustomDiff test custom diff behaviour.
func TestResourceWithCustomDiff(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: resourceWithCustomDiffConfig(false),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("test_resource_with_custom_diff.foo", "computed", "1"),
resource.TestCheckResourceAttr("test_resource_with_custom_diff.foo", "index", "1"),
resource.TestCheckResourceAttr("test_resource_with_custom_diff.foo", "list.#", "1"),
resource.TestCheckResourceAttr("test_resource_with_custom_diff.foo", "list.0", "dc1"),
),
ExpectNonEmptyPlan: true,
},
{
Config: resourceWithCustomDiffConfig(false),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("test_resource_with_custom_diff.foo", "computed", "2"),
resource.TestCheckResourceAttr("test_resource_with_custom_diff.foo", "index", "2"),
resource.TestCheckResourceAttr("test_resource_with_custom_diff.foo", "list.#", "2"),
resource.TestCheckResourceAttr("test_resource_with_custom_diff.foo", "list.0", "dc2"),
resource.TestCheckResourceAttr("test_resource_with_custom_diff.foo", "list.1", "dc3"),
resource.TestCheckNoResourceAttr("test_resource_with_custom_diff.foo", "list.2"),
),
ExpectNonEmptyPlan: true,
},
{
Config: resourceWithCustomDiffConfig(true),
ExpectError: regexp.MustCompile("veto is true, diff vetoed"),
},
},
})
}
func resourceWithCustomDiffConfig(veto bool) string {
return fmt.Sprintf(`
resource "test_resource_with_custom_diff" "foo" {
required = "yep"
veto = %t
}
`, veto)
}

View File

@ -65,7 +65,7 @@ func (b *Backend) Configure(c *terraform.ResourceConfig) error {
// Get a ResourceData for this configuration. To do this, we actually // Get a ResourceData for this configuration. To do this, we actually
// generate an intermediary "diff" although that is never exposed. // generate an intermediary "diff" although that is never exposed.
diff, err := sm.Diff(nil, c) diff, err := sm.Diff(nil, c, nil, nil)
if err != nil { if err != nil {
return err return err
} }

View File

@ -251,7 +251,7 @@ func (p *Provider) Configure(c *terraform.ResourceConfig) error {
// Get a ResourceData for this configuration. To do this, we actually // Get a ResourceData for this configuration. To do this, we actually
// generate an intermediary "diff" although that is never exposed. // generate an intermediary "diff" although that is never exposed.
diff, err := sm.Diff(nil, c) diff, err := sm.Diff(nil, c, nil, p.meta)
if err != nil { if err != nil {
return err return err
} }
@ -293,7 +293,7 @@ func (p *Provider) Diff(
return nil, fmt.Errorf("unknown resource type: %s", info.Type) return nil, fmt.Errorf("unknown resource type: %s", info.Type)
} }
return r.Diff(s, c) return r.Diff(s, c, p.meta)
} }
// Refresh implementation of terraform.ResourceProvider interface. // Refresh implementation of terraform.ResourceProvider interface.
@ -410,7 +410,7 @@ func (p *Provider) ReadDataDiff(
return nil, fmt.Errorf("unknown data source: %s", info.Type) return nil, fmt.Errorf("unknown data source: %s", info.Type)
} }
return r.Diff(nil, c) return r.Diff(nil, c, p.meta)
} }
// RefreshData implementation of terraform.ResourceProvider interface. // RefreshData implementation of terraform.ResourceProvider interface.

View File

@ -146,7 +146,7 @@ func (p *Provisioner) Apply(
} }
sm := schemaMap(p.ConnSchema) sm := schemaMap(p.ConnSchema)
diff, err := sm.Diff(nil, terraform.NewResourceConfig(c)) diff, err := sm.Diff(nil, terraform.NewResourceConfig(c), nil, nil)
if err != nil { if err != nil {
return err return err
} }
@ -160,7 +160,7 @@ func (p *Provisioner) Apply(
// Build the configuration data. Doing this requires making a "diff" // Build the configuration data. Doing this requires making a "diff"
// even though that's never used. We use that just to get the correct types. // even though that's never used. We use that just to get the correct types.
configMap := schemaMap(p.Schema) configMap := schemaMap(p.Schema)
diff, err := configMap.Diff(nil, c) diff, err := configMap.Diff(nil, c, nil, nil)
if err != nil { if err != nil {
return err return err
} }

View File

@ -85,6 +85,37 @@ type Resource struct {
Delete DeleteFunc Delete DeleteFunc
Exists ExistsFunc Exists ExistsFunc
// CustomizeDiff is a custom function for working with the diff that
// Terraform has created for this resource - it can be used to customize the
// diff that has been created, diff values not controlled by configuration,
// or even veto the diff altogether and abort the plan. It is passed a
// *ResourceDiff, a structure similar to ResourceData but lacking most write
// functions like Set, while introducing new functions that work with the
// diff such as SetNew, SetNewComputed, and ForceNew.
//
// The phases Terraform runs this in, and the state available via functions
// like Get and GetChange, are as follows:
//
// * New resource: One run with no state
// * Existing resource: One run with state
// * Existing resource, forced new: One run with state (before ForceNew),
// then one run without state (as if new resource)
// * Tainted resource: No runs (custom diff logic is skipped)
// * Destroy: No runs (standard diff logic is skipped on destroy diffs)
//
// This function needs to be resilient to support all scenarios.
//
// If this function needs to access external API resources, remember to flag
// the RequiresRefresh attribute mentioned below to ensure that
// -refresh=false is blocked when running plan or apply, as this means that
// this resource requires refresh-like behaviour to work effectively.
//
// For the most part, only computed fields can be customized by this
// function.
//
// This function is only allowed on regular resources (not data sources).
CustomizeDiff CustomizeDiffFunc
// Importer is the ResourceImporter implementation for this resource. // Importer is the ResourceImporter implementation for this resource.
// If this is nil, then this resource does not support importing. If // If this is nil, then this resource does not support importing. If
// this is non-nil, then it supports importing and ResourceImporter // this is non-nil, then it supports importing and ResourceImporter
@ -126,6 +157,9 @@ type ExistsFunc func(*ResourceData, interface{}) (bool, error)
type StateMigrateFunc func( type StateMigrateFunc func(
int, *terraform.InstanceState, interface{}) (*terraform.InstanceState, error) int, *terraform.InstanceState, interface{}) (*terraform.InstanceState, error)
// See Resource documentation.
type CustomizeDiffFunc func(*ResourceDiff, interface{}) error
// Apply creates, updates, and/or deletes a resource. // Apply creates, updates, and/or deletes a resource.
func (r *Resource) Apply( func (r *Resource) Apply(
s *terraform.InstanceState, s *terraform.InstanceState,
@ -202,11 +236,11 @@ func (r *Resource) Apply(
return r.recordCurrentSchemaVersion(data.State()), err return r.recordCurrentSchemaVersion(data.State()), err
} }
// Diff returns a diff of this resource and is API compatible with the // Diff returns a diff of this resource.
// ResourceProvider interface.
func (r *Resource) Diff( func (r *Resource) Diff(
s *terraform.InstanceState, s *terraform.InstanceState,
c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) { c *terraform.ResourceConfig,
meta interface{}) (*terraform.InstanceDiff, error) {
t := &ResourceTimeout{} t := &ResourceTimeout{}
err := t.ConfigDecode(r, c) err := t.ConfigDecode(r, c)
@ -215,7 +249,7 @@ func (r *Resource) Diff(
return nil, fmt.Errorf("[ERR] Error decoding timeout: %s", err) return nil, fmt.Errorf("[ERR] Error decoding timeout: %s", err)
} }
instanceDiff, err := schemaMap(r.Schema).Diff(s, c) instanceDiff, err := schemaMap(r.Schema).Diff(s, c, r.CustomizeDiff, meta)
if err != nil { if err != nil {
return instanceDiff, err return instanceDiff, err
} }
@ -346,6 +380,11 @@ func (r *Resource) InternalValidate(topSchemaMap schemaMap, writable bool) error
if r.Create != nil || r.Update != nil || r.Delete != nil { if r.Create != nil || r.Update != nil || r.Delete != nil {
return fmt.Errorf("must not implement Create, Update or Delete") return fmt.Errorf("must not implement Create, Update or Delete")
} }
// CustomizeDiff cannot be defined for read-only resources
if r.CustomizeDiff != nil {
return fmt.Errorf("cannot implement CustomizeDiff")
}
} }
tsm := topSchemaMap tsm := topSchemaMap

View File

@ -0,0 +1,469 @@
package schema
import (
"errors"
"fmt"
"reflect"
"strings"
"sync"
"github.com/hashicorp/terraform/terraform"
)
// newValueWriter is a minor re-implementation of MapFieldWriter to include
// keys that should be marked as computed, to represent the new part of a
// pseudo-diff.
type newValueWriter struct {
*MapFieldWriter
// A list of keys that should be marked as computed.
computedKeys map[string]bool
// A lock to prevent races on writes. The underlying writer will have one as
// well - this is for computed keys.
lock sync.Mutex
// To be used with init.
once sync.Once
}
// init performs any initialization tasks for the newValueWriter.
func (w *newValueWriter) init() {
if w.computedKeys == nil {
w.computedKeys = make(map[string]bool)
}
}
// WriteField overrides MapValueWriter's WriteField, adding the ability to flag
// the address as computed.
func (w *newValueWriter) WriteField(address []string, value interface{}, computed bool) error {
// Fail the write if we have a non-nil value and computed is true.
// NewComputed values should not have a value when written.
if value != nil && computed {
return errors.New("Non-nil value with computed set")
}
if err := w.MapFieldWriter.WriteField(address, value); err != nil {
return err
}
w.once.Do(w.init)
w.lock.Lock()
defer w.lock.Unlock()
if computed {
w.computedKeys[strings.Join(address, ".")] = true
}
return nil
}
// ComputedKeysMap returns the underlying computed keys map.
func (w *newValueWriter) ComputedKeysMap() map[string]bool {
w.once.Do(w.init)
return w.computedKeys
}
// newValueReader is a minor re-implementation of MapFieldReader and is the
// read counterpart to MapValueWriter, allowing the read of keys flagged as
// computed to accommodate the diff override logic in ResourceDiff.
type newValueReader struct {
*MapFieldReader
// The list of computed keys from a newValueWriter.
computedKeys map[string]bool
}
// ReadField reads the values from the underlying writer, returning the
// computed value if it is found as well.
func (r *newValueReader) ReadField(address []string) (FieldReadResult, error) {
addrKey := strings.Join(address, ".")
v, err := r.MapFieldReader.ReadField(address)
if err != nil {
return FieldReadResult{}, err
}
for computedKey := range r.computedKeys {
if childAddrOf(addrKey, computedKey) {
if strings.HasSuffix(addrKey, ".#") {
// This is a count value for a list or set that has been marked as
// computed, or a sub-list/sub-set of a complex resource that has
// been marked as computed. We need to pass through to other readers
// so that an accurate previous count can be fetched for the diff.
v.Exists = false
}
v.Computed = true
}
}
return v, nil
}
// ResourceDiff is used to query and make custom changes to an in-flight diff.
// It can be used to veto particular changes in the diff, customize the diff
// that has been created, or diff values not controlled by config.
//
// The object functions similar to ResourceData, however most notably lacks
// Set, SetPartial, and Partial, as it should be used to change diff values
// only. Most other first-class ResourceData functions exist, namely Get,
// GetOk, HasChange, and GetChange exist.
//
// All functions in ResourceDiff, save for ForceNew, can only be used on
// computed fields.
type ResourceDiff struct {
// The schema for the resource being worked on.
schema map[string]*Schema
// The current config for this resource.
config *terraform.ResourceConfig
// The state for this resource as it exists post-refresh, after the initial
// diff.
state *terraform.InstanceState
// The diff created by Terraform. This diff is used, along with state,
// config, and custom-set diff data, to provide a multi-level reader
// experience similar to ResourceData.
diff *terraform.InstanceDiff
// The internal reader structure that contains the state, config, the default
// diff, and the new diff.
multiReader *MultiLevelFieldReader
// A writer that writes overridden new fields.
newWriter *newValueWriter
// Tracks which keys have been updated by ResourceDiff to ensure that the
// diff does not get re-run on keys that were not touched, or diffs that were
// just removed (re-running on the latter would just roll back the removal).
updatedKeys map[string]bool
}
// newResourceDiff creates a new ResourceDiff instance.
func newResourceDiff(schema map[string]*Schema, config *terraform.ResourceConfig, state *terraform.InstanceState, diff *terraform.InstanceDiff) *ResourceDiff {
d := &ResourceDiff{
config: config,
state: state,
diff: diff,
schema: schema,
}
d.newWriter = &newValueWriter{
MapFieldWriter: &MapFieldWriter{Schema: d.schema},
}
readers := make(map[string]FieldReader)
var stateAttributes map[string]string
if d.state != nil {
stateAttributes = d.state.Attributes
readers["state"] = &MapFieldReader{
Schema: d.schema,
Map: BasicMapReader(stateAttributes),
}
}
if d.config != nil {
readers["config"] = &ConfigFieldReader{
Schema: d.schema,
Config: d.config,
}
}
if d.diff != nil {
readers["diff"] = &DiffFieldReader{
Schema: d.schema,
Diff: d.diff,
Source: &MultiLevelFieldReader{
Levels: []string{"state", "config"},
Readers: readers,
},
}
}
readers["newDiff"] = &newValueReader{
MapFieldReader: &MapFieldReader{
Schema: d.schema,
Map: BasicMapReader(d.newWriter.Map()),
},
computedKeys: d.newWriter.ComputedKeysMap(),
}
d.multiReader = &MultiLevelFieldReader{
Levels: []string{
"state",
"config",
"diff",
"newDiff",
},
Readers: readers,
}
d.updatedKeys = make(map[string]bool)
return d
}
// UpdatedKeys returns the keys that were updated by this ResourceDiff run.
// These are the only keys that a diff should be re-calculated for.
func (d *ResourceDiff) UpdatedKeys() []string {
var s []string
for k := range d.updatedKeys {
s = append(s, k)
}
return s
}
// Clear wipes the diff for a particular key. It is called by ResourceDiff's
// functionality to remove any possibility of conflicts, but can be called on
// its own to just remove a specific key from the diff completely.
//
// Note that this does not wipe an override. This function is only allowed on
// computed keys.
func (d *ResourceDiff) Clear(key string) error {
if err := d.checkKey(key, "Clear"); err != nil {
return err
}
return d.clear(key)
}
func (d *ResourceDiff) clear(key string) error {
// Check the schema to make sure that this key exists first.
if _, ok := d.schema[key]; !ok {
return fmt.Errorf("%s is not a valid key", key)
}
for k := range d.diff.Attributes {
if strings.HasPrefix(k, key) {
delete(d.diff.Attributes, k)
}
}
return nil
}
// diffChange helps to implement resourceDiffer and derives its change values
// from ResourceDiff's own change data, in addition to existing diff, config, and state.
func (d *ResourceDiff) diffChange(key string) (interface{}, interface{}, bool, bool) {
old, new := d.getChange(key)
if !old.Exists {
old.Value = nil
}
if !new.Exists {
new.Value = nil
}
return old.Value, new.Value, !reflect.DeepEqual(old.Value, new.Value), new.Computed
}
// SetNew is used to set a new diff value for the mentioned key. The value must
// be correct for the attribute's schema (mostly relevant for maps, lists, and
// sets). The original value from the state is used as the old value.
//
// This function is only allowed on computed attributes.
func (d *ResourceDiff) SetNew(key string, value interface{}) error {
if err := d.checkKey(key, "SetNew"); err != nil {
return err
}
return d.setDiff(key, value, false)
}
// SetNewComputed functions like SetNew, except that it blanks out a new value
// and marks it as computed.
//
// This function is only allowed on computed attributes.
func (d *ResourceDiff) SetNewComputed(key string) error {
if err := d.checkKey(key, "SetNewComputed"); err != nil {
return err
}
return d.setDiff(key, nil, true)
}
// setDiff performs common diff setting behaviour.
func (d *ResourceDiff) setDiff(key string, new interface{}, computed bool) error {
if err := d.clear(key); err != nil {
return err
}
if err := d.newWriter.WriteField(strings.Split(key, "."), new, computed); err != nil {
return fmt.Errorf("Cannot set new diff value for key %s: %s", key, err)
}
d.updatedKeys[key] = true
return nil
}
// ForceNew force-flags ForceNew in the schema for a specific key, and
// re-calculates its diff, effectively causing this attribute to force a new
// resource.
//
// Keep in mind that forcing a new resource will force a second run of the
// resource's CustomizeDiff function (with a new ResourceDiff) once the current
// one has completed. This second run is performed without state. This behavior
// will be the same as if a new resource is being created and is performed to
// ensure that the diff looks like the diff for a new resource as much as
// possible. CustomizeDiff should expect such a scenario and act correctly.
//
// This function is a no-op/error if there is no diff.
//
// Note that the change to schema is permanent for the lifecycle of this
// specific ResourceDiff instance.
func (d *ResourceDiff) ForceNew(key string) error {
if !d.HasChange(key) {
return fmt.Errorf("ForceNew: No changes for %s", key)
}
_, new := d.GetChange(key)
d.schema[key].ForceNew = true
return d.setDiff(key, new, false)
}
// Get hands off to ResourceData.Get.
func (d *ResourceDiff) Get(key string) interface{} {
r, _ := d.GetOk(key)
return r
}
// GetChange gets the change between the state and diff, checking first to see
// if a overridden diff exists.
//
// This implementation differs from ResourceData's in the way that we first get
// results from the exact levels for the new diff, then from state and diff as
// per normal.
func (d *ResourceDiff) GetChange(key string) (interface{}, interface{}) {
old, new := d.getChange(key)
return old.Value, new.Value
}
// GetOk functions the same way as ResourceData.GetOk, but it also checks the
// new diff levels to provide data consistent with the current state of the
// customized diff.
func (d *ResourceDiff) GetOk(key string) (interface{}, bool) {
r := d.get(strings.Split(key, "."), "newDiff")
exists := r.Exists && !r.Computed
if exists {
// If it exists, we also want to verify it is not the zero-value.
value := r.Value
zero := r.Schema.Type.Zero()
if eq, ok := value.(Equal); ok {
exists = !eq.Equal(zero)
} else {
exists = !reflect.DeepEqual(value, zero)
}
}
return r.Value, exists
}
// HasChange checks to see if there is a change between state and the diff, or
// in the overridden diff.
func (d *ResourceDiff) HasChange(key string) bool {
old, new := d.GetChange(key)
// If the type implements the Equal interface, then call that
// instead of just doing a reflect.DeepEqual. An example where this is
// needed is *Set
if eq, ok := old.(Equal); ok {
return !eq.Equal(new)
}
return !reflect.DeepEqual(old, new)
}
// Id returns the ID of this resource.
//
// Note that technically, ID does not change during diffs (it either has
// already changed in the refresh, or will change on update), hence we do not
// support updating the ID or fetching it from anything else other than state.
func (d *ResourceDiff) Id() string {
var result string
if d.state != nil {
result = d.state.ID
}
return result
}
// getChange gets values from two different levels, designed for use in
// diffChange, HasChange, and GetChange.
//
// This implementation differs from ResourceData's in the way that we first get
// results from the exact levels for the new diff, then from state and diff as
// per normal.
func (d *ResourceDiff) getChange(key string) (getResult, getResult) {
old := d.get(strings.Split(key, "."), "state")
var new getResult
for p := range d.updatedKeys {
if childAddrOf(key, p) {
new = d.getExact(strings.Split(key, "."), "newDiff")
goto done
}
}
new = d.get(strings.Split(key, "."), "newDiff")
done:
return old, new
}
// get performs the appropriate multi-level reader logic for ResourceDiff,
// starting at source. Refer to newResourceDiff for the level order.
func (d *ResourceDiff) get(addr []string, source string) getResult {
result, err := d.multiReader.ReadFieldMerge(addr, source)
if err != nil {
panic(err)
}
return d.finalizeResult(addr, result)
}
// getExact gets an attribute from the exact level referenced by source.
func (d *ResourceDiff) getExact(addr []string, source string) getResult {
result, err := d.multiReader.ReadFieldExact(addr, source)
if err != nil {
panic(err)
}
return d.finalizeResult(addr, result)
}
// finalizeResult does some post-processing of the result produced by get and getExact.
func (d *ResourceDiff) finalizeResult(addr []string, result FieldReadResult) getResult {
// If the result doesn't exist, then we set the value to the zero value
var schema *Schema
if schemaL := addrToSchema(addr, d.schema); len(schemaL) > 0 {
schema = schemaL[len(schemaL)-1]
}
if result.Value == nil && schema != nil {
result.Value = result.ValueOrZero(schema)
}
// Transform the FieldReadResult into a getResult. It might be worth
// merging these two structures one day.
return getResult{
Value: result.Value,
ValueProcessed: result.ValueProcessed,
Computed: result.Computed,
Exists: result.Exists,
Schema: schema,
}
}
// childAddrOf does a comparison of two addresses to see if one is the child of
// the other.
func childAddrOf(child, parent string) bool {
cs := strings.Split(child, ".")
ps := strings.Split(parent, ".")
if len(ps) > len(cs) {
return false
}
return reflect.DeepEqual(ps, cs[:len(ps)])
}
// checkKey checks the key to make sure it exists and is computed.
func (d *ResourceDiff) checkKey(key, caller string) error {
s, ok := d.schema[key]
if !ok {
return fmt.Errorf("%s: invalid key: %s", caller, key)
}
if !s.Computed {
return fmt.Errorf("%s only operates on computed keys - %s is not one", caller, key)
}
return nil
}

View File

@ -0,0 +1,853 @@
package schema
import (
"reflect"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/hashicorp/terraform/terraform"
)
// testSetFunc is a very simple function we use to test a foo/bar complex set.
// Both "foo" and "bar" are int values.
//
// This is not foolproof as since it performs sums, you can run into
// collisions. Spec tests accordingly. :P
func testSetFunc(v interface{}) int {
m := v.(map[string]interface{})
return m["foo"].(int) + m["bar"].(int)
}
// resourceDiffTestCase provides a test case struct for SetNew and SetDiff.
type resourceDiffTestCase struct {
Name string
Schema map[string]*Schema
State *terraform.InstanceState
Config *terraform.ResourceConfig
Diff *terraform.InstanceDiff
Key string
OldValue interface{}
NewValue interface{}
Expected *terraform.InstanceDiff
ExpectedError bool
}
// testDiffCases produces a list of test cases for use with SetNew and SetDiff.
func testDiffCases(t *testing.T, oldPrefix string, oldOffset int, computed bool) []resourceDiffTestCase {
return []resourceDiffTestCase{
resourceDiffTestCase{
Name: "basic primitive diff",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"foo": "bar",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": "baz",
}),
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
},
},
Key: "foo",
NewValue: "qux",
Expected: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{
Old: "bar",
New: func() string {
if computed {
return ""
}
return "qux"
}(),
NewComputed: computed,
},
},
},
},
resourceDiffTestCase{
Name: "basic set diff",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeSet,
Optional: true,
Computed: true,
Elem: &Schema{Type: TypeString},
Set: HashString,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"foo.#": "1",
"foo.1996459178": "bar",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": []interface{}{"baz"},
}),
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo.1996459178": &terraform.ResourceAttrDiff{
Old: "bar",
New: "",
NewRemoved: true,
},
"foo.2015626392": &terraform.ResourceAttrDiff{
Old: "",
New: "baz",
},
},
},
Key: "foo",
NewValue: []interface{}{"qux"},
Expected: &terraform.InstanceDiff{
Attributes: func() map[string]*terraform.ResourceAttrDiff {
result := map[string]*terraform.ResourceAttrDiff{}
if computed {
result["foo.#"] = &terraform.ResourceAttrDiff{
Old: "1",
New: "",
NewComputed: true,
}
} else {
result["foo.2800005064"] = &terraform.ResourceAttrDiff{
Old: "",
New: "qux",
}
result["foo.1996459178"] = &terraform.ResourceAttrDiff{
Old: "bar",
New: "",
NewRemoved: true,
}
}
return result
}(),
},
},
resourceDiffTestCase{
Name: "basic list diff",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeList,
Optional: true,
Computed: true,
Elem: &Schema{Type: TypeString},
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"foo.#": "1",
"foo.0": "bar",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": []interface{}{"baz"},
}),
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo.0": &terraform.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
},
},
Key: "foo",
NewValue: []interface{}{"qux"},
Expected: &terraform.InstanceDiff{
Attributes: func() map[string]*terraform.ResourceAttrDiff {
result := make(map[string]*terraform.ResourceAttrDiff)
if computed {
result["foo.#"] = &terraform.ResourceAttrDiff{
Old: "1",
New: "",
NewComputed: true,
}
} else {
result["foo.0"] = &terraform.ResourceAttrDiff{
Old: "bar",
New: "qux",
}
}
return result
}(),
},
},
resourceDiffTestCase{
Name: "basic map diff",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeMap,
Optional: true,
Computed: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"foo.%": "1",
"foo.bar": "baz",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": map[string]interface{}{"bar": "qux"},
}),
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo.bar": &terraform.ResourceAttrDiff{
Old: "baz",
New: "qux",
},
},
},
Key: "foo",
NewValue: map[string]interface{}{"bar": "quux"},
Expected: &terraform.InstanceDiff{
Attributes: func() map[string]*terraform.ResourceAttrDiff {
result := make(map[string]*terraform.ResourceAttrDiff)
if computed {
result["foo.%"] = &terraform.ResourceAttrDiff{
Old: "",
New: "",
NewComputed: true,
}
result["foo.bar"] = &terraform.ResourceAttrDiff{
Old: "baz",
New: "",
NewRemoved: true,
}
} else {
result["foo.bar"] = &terraform.ResourceAttrDiff{
Old: "baz",
New: "quux",
}
}
return result
}(),
},
},
resourceDiffTestCase{
Name: "additional diff with primitive",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Optional: true,
},
"one": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"foo": "bar",
"one": "two",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": "baz",
}),
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
},
},
Key: "one",
NewValue: "four",
Expected: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
"one": &terraform.ResourceAttrDiff{
Old: "two",
New: func() string {
if computed {
return ""
}
return "four"
}(),
NewComputed: computed,
},
},
},
},
resourceDiffTestCase{
Name: "additional diff with primitive computed only",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Optional: true,
},
"one": &Schema{
Type: TypeString,
Computed: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"foo": "bar",
"one": "two",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": "baz",
}),
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
},
},
Key: "one",
NewValue: "three",
Expected: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
"one": &terraform.ResourceAttrDiff{
Old: "two",
New: func() string {
if computed {
return ""
}
return "three"
}(),
NewComputed: computed,
},
},
},
},
resourceDiffTestCase{
Name: "complex-ish set diff",
Schema: map[string]*Schema{
"top": &Schema{
Type: TypeSet,
Optional: true,
Computed: true,
Elem: &Resource{
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Optional: true,
Computed: true,
},
"bar": &Schema{
Type: TypeInt,
Optional: true,
Computed: true,
},
},
},
Set: testSetFunc,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"top.#": "2",
"top.3.foo": "1",
"top.3.bar": "2",
"top.23.foo": "11",
"top.23.bar": "12",
},
},
Config: testConfig(t, map[string]interface{}{
"top": []interface{}{
map[string]interface{}{
"foo": 1,
"bar": 3,
},
map[string]interface{}{
"foo": 12,
"bar": 12,
},
},
}),
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"top.4.foo": &terraform.ResourceAttrDiff{
Old: "",
New: "1",
},
"top.4.bar": &terraform.ResourceAttrDiff{
Old: "",
New: "3",
},
"top.24.foo": &terraform.ResourceAttrDiff{
Old: "",
New: "12",
},
"top.24.bar": &terraform.ResourceAttrDiff{
Old: "",
New: "12",
},
},
},
Key: "top",
NewValue: NewSet(testSetFunc, []interface{}{
map[string]interface{}{
"foo": 1,
"bar": 4,
},
map[string]interface{}{
"foo": 13,
"bar": 12,
},
map[string]interface{}{
"foo": 21,
"bar": 22,
},
}),
Expected: &terraform.InstanceDiff{
Attributes: func() map[string]*terraform.ResourceAttrDiff {
result := make(map[string]*terraform.ResourceAttrDiff)
if computed {
result["top.#"] = &terraform.ResourceAttrDiff{
Old: "2",
New: "",
NewComputed: true,
}
} else {
result["top.#"] = &terraform.ResourceAttrDiff{
Old: "2",
New: "3",
}
result["top.5.foo"] = &terraform.ResourceAttrDiff{
Old: "",
New: "1",
}
result["top.5.bar"] = &terraform.ResourceAttrDiff{
Old: "",
New: "4",
}
result["top.25.foo"] = &terraform.ResourceAttrDiff{
Old: "",
New: "13",
}
result["top.25.bar"] = &terraform.ResourceAttrDiff{
Old: "",
New: "12",
}
result["top.43.foo"] = &terraform.ResourceAttrDiff{
Old: "",
New: "21",
}
result["top.43.bar"] = &terraform.ResourceAttrDiff{
Old: "",
New: "22",
}
}
return result
}(),
},
},
resourceDiffTestCase{
Name: "primitive, no diff, no refresh",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Computed: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"foo": "bar",
},
},
Config: testConfig(t, map[string]interface{}{}),
Diff: &terraform.InstanceDiff{Attributes: map[string]*terraform.ResourceAttrDiff{}},
Key: "foo",
NewValue: "baz",
Expected: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{
Old: "bar",
New: func() string {
if computed {
return ""
}
return "baz"
}(),
NewComputed: computed,
},
},
},
},
resourceDiffTestCase{
Name: "non-computed key, should error",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Required: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"foo": "bar",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": "baz",
}),
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
},
},
Key: "foo",
NewValue: "qux",
ExpectedError: true,
},
resourceDiffTestCase{
Name: "bad key, should error",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Required: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"foo": "bar",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": "baz",
}),
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
},
},
Key: "bad",
NewValue: "qux",
ExpectedError: true,
},
}
}
func TestSetNew(t *testing.T) {
testCases := testDiffCases(t, "", 0, false)
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
m := schemaMap(tc.Schema)
d := newResourceDiff(tc.Schema, tc.Config, tc.State, tc.Diff)
err := d.SetNew(tc.Key, tc.NewValue)
switch {
case err != nil && !tc.ExpectedError:
t.Fatalf("bad: %s", err)
case err == nil && tc.ExpectedError:
t.Fatalf("Expected error, got none")
case err != nil && tc.ExpectedError:
return
}
for _, k := range d.UpdatedKeys() {
if err := m.diff(k, m[k], tc.Diff, d, false); err != nil {
t.Fatalf("bad: %s", err)
}
}
if !reflect.DeepEqual(tc.Expected, tc.Diff) {
t.Fatalf("Expected %s, got %s", spew.Sdump(tc.Expected), spew.Sdump(tc.Diff))
}
})
}
}
func TestSetNewComputed(t *testing.T) {
testCases := testDiffCases(t, "", 0, true)
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
m := schemaMap(tc.Schema)
d := newResourceDiff(tc.Schema, tc.Config, tc.State, tc.Diff)
err := d.SetNewComputed(tc.Key)
switch {
case err != nil && !tc.ExpectedError:
t.Fatalf("bad: %s", err)
case err == nil && tc.ExpectedError:
t.Fatalf("Expected error, got none")
case err != nil && tc.ExpectedError:
return
}
for _, k := range d.UpdatedKeys() {
if err := m.diff(k, m[k], tc.Diff, d, false); err != nil {
t.Fatalf("bad: %s", err)
}
}
if !reflect.DeepEqual(tc.Expected, tc.Diff) {
t.Fatalf("Expected %s, got %s", spew.Sdump(tc.Expected), spew.Sdump(tc.Diff))
}
})
}
}
func TestForceNew(t *testing.T) {
cases := []resourceDiffTestCase{
resourceDiffTestCase{
Name: "basic primitive diff",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"foo": "bar",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": "baz",
}),
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
},
},
Key: "foo",
Expected: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{
Old: "bar",
New: "baz",
RequiresNew: true,
},
},
},
},
resourceDiffTestCase{
Name: "no change, should error",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"foo": "bar",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": "bar",
}),
ExpectedError: true,
},
resourceDiffTestCase{
Name: "basic primitive, non-computed key",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Required: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"foo": "bar",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": "baz",
}),
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
},
},
Key: "foo",
Expected: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{
Old: "bar",
New: "baz",
RequiresNew: true,
},
},
},
},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
m := schemaMap(tc.Schema)
d := newResourceDiff(m, tc.Config, tc.State, tc.Diff)
err := d.ForceNew(tc.Key)
switch {
case err != nil && !tc.ExpectedError:
t.Fatalf("bad: %s", err)
case err == nil && tc.ExpectedError:
t.Fatalf("Expected error, got none")
case err != nil && tc.ExpectedError:
return
}
for _, k := range d.UpdatedKeys() {
if err := m.diff(k, m[k], tc.Diff, d, false); err != nil {
t.Fatalf("bad: %s", err)
}
}
if !reflect.DeepEqual(tc.Expected, tc.Diff) {
t.Fatalf("Expected %s, got %s", spew.Sdump(tc.Expected), spew.Sdump(tc.Diff))
}
})
}
}
func TestClear(t *testing.T) {
cases := []resourceDiffTestCase{
resourceDiffTestCase{
Name: "basic primitive diff",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"foo": "bar",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": "baz",
}),
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
},
},
Key: "foo",
Expected: &terraform.InstanceDiff{Attributes: map[string]*terraform.ResourceAttrDiff{}},
},
resourceDiffTestCase{
Name: "non-computed key, should error",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Required: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"foo": "bar",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": "baz",
}),
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
},
},
Key: "foo",
ExpectedError: true,
},
resourceDiffTestCase{
Name: "multi-value, one removed",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
"one": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"foo": "bar",
"one": "two",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": "baz",
"one": "three",
}),
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
"one": &terraform.ResourceAttrDiff{
Old: "two",
New: "three",
},
},
},
Key: "one",
Expected: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
},
},
},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
m := schemaMap(tc.Schema)
d := newResourceDiff(m, tc.Config, tc.State, tc.Diff)
err := d.Clear(tc.Key)
switch {
case err != nil && !tc.ExpectedError:
t.Fatalf("bad: %s", err)
case err == nil && tc.ExpectedError:
t.Fatalf("Expected error, got none")
case err != nil && tc.ExpectedError:
return
}
for _, k := range d.UpdatedKeys() {
if err := m.diff(k, m[k], tc.Diff, d, false); err != nil {
t.Fatalf("bad: %s", err)
}
}
if !reflect.DeepEqual(tc.Expected, tc.Diff) {
t.Fatalf("Expected %s, got %s", spew.Sdump(tc.Expected), spew.Sdump(tc.Diff))
}
})
}
}

View File

@ -226,7 +226,7 @@ func TestResourceDiff_Timeout_diff(t *testing.T) {
var s *terraform.InstanceState = nil var s *terraform.InstanceState = nil
conf := terraform.NewResourceConfig(raw) conf := terraform.NewResourceConfig(raw)
actual, err := r.Diff(s, conf) actual, err := r.Diff(s, conf, nil)
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
@ -254,6 +254,44 @@ func TestResourceDiff_Timeout_diff(t *testing.T) {
} }
} }
func TestResourceDiff_CustomizeFunc(t *testing.T) {
r := &Resource{
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Optional: true,
},
},
}
var called bool
r.CustomizeDiff = func(d *ResourceDiff, m interface{}) error {
called = true
return nil
}
raw, err := config.NewRawConfig(
map[string]interface{}{
"foo": 42,
})
if err != nil {
t.Fatalf("err: %s", err)
}
var s *terraform.InstanceState
conf := terraform.NewResourceConfig(raw)
_, err = r.Diff(s, conf, nil)
if err != nil {
t.Fatalf("err: %s", err)
}
if !called {
t.Fatalf("diff customization not called")
}
}
func TestResourceApply_destroy(t *testing.T) { func TestResourceApply_destroy(t *testing.T) {
r := &Resource{ r := &Resource{
Schema: map[string]*Schema{ Schema: map[string]*Schema{
@ -797,6 +835,21 @@ func TestResourceInternalValidate(t *testing.T) {
true, true,
false, false,
}, },
13: { // non-writable must not define CustomizeDiff
&Resource{
Read: func(d *ResourceData, meta interface{}) error { return nil },
Schema: map[string]*Schema{
"goo": &Schema{
Type: TypeInt,
Optional: true,
},
},
CustomizeDiff: func(*ResourceDiff, interface{}) error { return nil },
},
false,
true,
},
} }
for i, tc := range cases { for i, tc := range cases {

View File

@ -21,6 +21,7 @@ import (
"strings" "strings"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/copystructure"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
) )
@ -333,7 +334,7 @@ func (s *Schema) finalizeDiff(
return d return d
} }
if s.Computed { if s.Computed && !d.NewComputed {
if d.Old != "" && d.New == "" { if d.Old != "" && d.New == "" {
// This is a computed value with an old value set already, // This is a computed value with an old value set already,
// just let it go. // just let it go.
@ -370,11 +371,23 @@ func (m schemaMap) Data(
}, nil }, nil
} }
// DeepCopy returns a copy of this schemaMap. The copy can be safely modified
// without affecting the original.
func (m *schemaMap) DeepCopy() schemaMap {
copy, err := copystructure.Config{Lock: true}.Copy(m)
if err != nil {
panic(err)
}
return copy.(schemaMap)
}
// Diff returns the diff for a resource given the schema map, // Diff returns the diff for a resource given the schema map,
// state, and configuration. // state, and configuration.
func (m schemaMap) Diff( func (m schemaMap) Diff(
s *terraform.InstanceState, s *terraform.InstanceState,
c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) { c *terraform.ResourceConfig,
customizeDiff CustomizeDiffFunc,
meta interface{}) (*terraform.InstanceDiff, error) {
result := new(terraform.InstanceDiff) result := new(terraform.InstanceDiff)
result.Attributes = make(map[string]*terraform.ResourceAttrDiff) result.Attributes = make(map[string]*terraform.ResourceAttrDiff)
@ -396,6 +409,22 @@ func (m schemaMap) Diff(
} }
} }
// If this is a non-destroy diff, call any custom diff logic that has been
// defined.
if !result.DestroyTainted && customizeDiff != nil {
mc := m.DeepCopy()
rd := newResourceDiff(mc, c, s, result)
if err := customizeDiff(rd, meta); err != nil {
return nil, err
}
for _, k := range rd.UpdatedKeys() {
err := m.diff(k, mc[k], result, rd, false)
if err != nil {
return nil, err
}
}
}
// If the diff requires a new resource, then we recompute the diff // If the diff requires a new resource, then we recompute the diff
// so we have the complete new resource diff, and preserve the // so we have the complete new resource diff, and preserve the
// RequiresNew fields where necessary so the user knows exactly what // RequiresNew fields where necessary so the user knows exactly what
@ -421,6 +450,21 @@ func (m schemaMap) Diff(
} }
} }
// Re-run customization
if !result2.DestroyTainted && customizeDiff != nil {
mc := m.DeepCopy()
rd := newResourceDiff(mc, c, d.state, result2)
if err := customizeDiff(rd, meta); err != nil {
return nil, err
}
for _, k := range rd.UpdatedKeys() {
err := m.diff(k, mc[k], result2, rd, false)
if err != nil {
return nil, err
}
}
}
// Force all the fields to not force a new since we know what we // Force all the fields to not force a new since we know what we
// want to force new. // want to force new.
for k, attr := range result2.Attributes { for k, attr := range result2.Attributes {
@ -684,11 +728,23 @@ func isValidFieldName(name string) bool {
return re.MatchString(name) return re.MatchString(name)
} }
// resourceDiffer is an interface that is used by the private diff functions.
// This helps facilitate diff logic for both ResourceData and ResoureDiff with
// minimal divergence in code.
type resourceDiffer interface {
diffChange(string) (interface{}, interface{}, bool, bool)
Get(string) interface{}
GetChange(string) (interface{}, interface{})
GetOk(string) (interface{}, bool)
HasChange(string) bool
Id() string
}
func (m schemaMap) diff( func (m schemaMap) diff(
k string, k string,
schema *Schema, schema *Schema,
diff *terraform.InstanceDiff, diff *terraform.InstanceDiff,
d *ResourceData, d resourceDiffer,
all bool) error { all bool) error {
unsupressedDiff := new(terraform.InstanceDiff) unsupressedDiff := new(terraform.InstanceDiff)
@ -709,12 +765,14 @@ func (m schemaMap) diff(
} }
for attrK, attrV := range unsupressedDiff.Attributes { for attrK, attrV := range unsupressedDiff.Attributes {
if schema.DiffSuppressFunc != nil && switch rd := d.(type) {
attrV != nil && case *ResourceData:
schema.DiffSuppressFunc(attrK, attrV.Old, attrV.New, d) { if schema.DiffSuppressFunc != nil &&
continue attrV != nil &&
schema.DiffSuppressFunc(attrK, attrV.Old, attrV.New, rd) {
continue
}
} }
diff.Attributes[attrK] = attrV diff.Attributes[attrK] = attrV
} }
@ -725,7 +783,7 @@ func (m schemaMap) diffList(
k string, k string,
schema *Schema, schema *Schema,
diff *terraform.InstanceDiff, diff *terraform.InstanceDiff,
d *ResourceData, d resourceDiffer,
all bool) error { all bool) error {
o, n, _, computedList := d.diffChange(k) o, n, _, computedList := d.diffChange(k)
if computedList { if computedList {
@ -844,7 +902,7 @@ func (m schemaMap) diffMap(
k string, k string,
schema *Schema, schema *Schema,
diff *terraform.InstanceDiff, diff *terraform.InstanceDiff,
d *ResourceData, d resourceDiffer,
all bool) error { all bool) error {
prefix := k + "." prefix := k + "."
@ -938,7 +996,7 @@ func (m schemaMap) diffSet(
k string, k string,
schema *Schema, schema *Schema,
diff *terraform.InstanceDiff, diff *terraform.InstanceDiff,
d *ResourceData, d resourceDiffer,
all bool) error { all bool) error {
o, n, _, computedSet := d.diffChange(k) o, n, _, computedSet := d.diffChange(k)
@ -1059,7 +1117,7 @@ func (m schemaMap) diffString(
k string, k string,
schema *Schema, schema *Schema,
diff *terraform.InstanceDiff, diff *terraform.InstanceDiff,
d *ResourceData, d resourceDiffer,
all bool) error { all bool) error {
var originalN interface{} var originalN interface{}
var os, ns string var os, ns string
@ -1093,7 +1151,7 @@ func (m schemaMap) diffString(
} }
removed := false removed := false
if o != nil && n == nil { if o != nil && n == nil && !computed {
removed = true removed = true
} }
if removed && schema.Computed { if removed && schema.Computed {

View File

@ -2,6 +2,7 @@ package schema
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"os" "os"
"reflect" "reflect"
@ -138,6 +139,7 @@ func TestSchemaMap_Diff(t *testing.T) {
State *terraform.InstanceState State *terraform.InstanceState
Config map[string]interface{} Config map[string]interface{}
ConfigVariables map[string]ast.Variable ConfigVariables map[string]ast.Variable
CustomizeDiff CustomizeDiffFunc
Diff *terraform.InstanceDiff Diff *terraform.InstanceDiff
Err bool Err bool
}{ }{
@ -2823,6 +2825,291 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false, Err: false,
}, },
{
Name: "overridden diff with a CustomizeDiff function, ForceNew not in schema",
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: nil,
Config: map[string]interface{}{
"availability_zone": "foo",
},
CustomizeDiff: func(d *ResourceDiff, meta interface{}) error {
if err := d.SetNew("availability_zone", "bar"); err != nil {
return err
}
if err := d.ForceNew("availability_zone"); err != nil {
return err
}
return nil
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": &terraform.ResourceAttrDiff{
Old: "",
New: "bar",
RequiresNew: true,
},
},
},
Err: false,
},
{
Name: "overridden diff with a CustomizeDiff function, ForceNew in schema",
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
State: nil,
Config: map[string]interface{}{
"availability_zone": "foo",
},
CustomizeDiff: func(d *ResourceDiff, meta interface{}) error {
if err := d.SetNew("availability_zone", "bar"); err != nil {
return err
}
return nil
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": &terraform.ResourceAttrDiff{
Old: "",
New: "bar",
RequiresNew: true,
},
},
},
Err: false,
},
{
Name: "required field with computed diff added with CustomizeDiff function",
Schema: map[string]*Schema{
"ami_id": &Schema{
Type: TypeString,
Required: true,
},
"instance_id": &Schema{
Type: TypeString,
Computed: true,
},
},
State: nil,
Config: map[string]interface{}{
"ami_id": "foo",
},
CustomizeDiff: func(d *ResourceDiff, meta interface{}) error {
if err := d.SetNew("instance_id", "bar"); err != nil {
return err
}
return nil
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ami_id": &terraform.ResourceAttrDiff{
Old: "",
New: "foo",
},
"instance_id": &terraform.ResourceAttrDiff{
Old: "",
New: "bar",
},
},
},
Err: false,
},
{
Name: "Set ForceNew only marks the changing element as ForceNew - CustomizeDiffFunc edition",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
Optional: true,
Computed: true,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"ports.#": "3",
"ports.1": "1",
"ports.2": "2",
"ports.4": "4",
},
},
Config: map[string]interface{}{
"ports": []interface{}{5, 2, 6},
},
CustomizeDiff: func(d *ResourceDiff, meta interface{}) error {
if err := d.SetNew("ports", []interface{}{5, 2, 1}); err != nil {
return err
}
if err := d.ForceNew("ports"); err != nil {
return err
}
return nil
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "3",
New: "3",
},
"ports.1": &terraform.ResourceAttrDiff{
Old: "1",
New: "1",
},
"ports.2": &terraform.ResourceAttrDiff{
Old: "2",
New: "2",
},
"ports.5": &terraform.ResourceAttrDiff{
Old: "",
New: "5",
RequiresNew: true,
},
"ports.4": &terraform.ResourceAttrDiff{
Old: "4",
New: "0",
NewRemoved: true,
RequiresNew: true,
},
},
},
},
{
Name: "tainted resource does not run CustomizeDiffFunc",
Schema: map[string]*Schema{},
State: &terraform.InstanceState{
Attributes: map[string]string{
"id": "someid",
},
Tainted: true,
},
Config: map[string]interface{}{},
CustomizeDiff: func(d *ResourceDiff, meta interface{}) error {
return errors.New("diff customization should not have run")
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{},
DestroyTainted: true,
},
Err: false,
},
{
Name: "NewComputed based on a conditional with CustomizeDiffFunc",
Schema: map[string]*Schema{
"etag": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
"version_id": &Schema{
Type: TypeString,
Computed: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"etag": "foo",
"version_id": "1",
},
},
Config: map[string]interface{}{
"etag": "bar",
},
CustomizeDiff: func(d *ResourceDiff, meta interface{}) error {
if d.HasChange("etag") {
d.SetNewComputed("version_id")
}
return nil
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"etag": &terraform.ResourceAttrDiff{
Old: "foo",
New: "bar",
},
"version_id": &terraform.ResourceAttrDiff{
Old: "1",
New: "",
NewComputed: true,
},
},
},
Err: false,
},
{
Name: "vetoing a diff",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"foo": "bar",
},
},
Config: map[string]interface{}{
"foo": "baz",
},
CustomizeDiff: func(d *ResourceDiff, meta interface{}) error {
return fmt.Errorf("diff vetoed")
},
Err: true,
},
} }
for i, tc := range cases { for i, tc := range cases {
@ -2838,8 +3125,7 @@ func TestSchemaMap_Diff(t *testing.T) {
} }
} }
d, err := schemaMap(tc.Schema).Diff( d, err := schemaMap(tc.Schema).Diff(tc.State, terraform.NewResourceConfig(c), tc.CustomizeDiff, nil)
tc.State, terraform.NewResourceConfig(c))
if err != nil != tc.Err { if err != nil != tc.Err {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
@ -3689,8 +3975,7 @@ func TestSchemaMap_DiffSuppress(t *testing.T) {
} }
} }
d, err := schemaMap(tc.Schema).Diff( d, err := schemaMap(tc.Schema).Diff(tc.State, terraform.NewResourceConfig(c), nil, nil)
tc.State, terraform.NewResourceConfig(c))
if err != nil != tc.Err { if err != nil != tc.Err {
t.Fatalf("#%q err: %s", tn, err) t.Fatalf("#%q err: %s", tn, err)
} }
@ -5022,3 +5307,17 @@ func (e errorSort) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
func (e errorSort) Less(i, j int) bool { func (e errorSort) Less(i, j int) bool {
return e[i].Error() < e[j].Error() return e[i].Error() < e[j].Error()
} }
func TestSchemaMapDeepCopy(t *testing.T) {
schema := map[string]*Schema{
"foo": &Schema{
Type: TypeString,
},
}
source := schemaMap(schema)
dest := source.DeepCopy()
dest["foo"].ForceNew = true
if reflect.DeepEqual(source, dest) {
t.Fatalf("source and dest should not match")
}
}

View File

@ -18,7 +18,7 @@ func TestResourceDataRaw(
} }
sm := schemaMap(schema) sm := schemaMap(schema)
diff, err := sm.Diff(nil, terraform.NewResourceConfig(c)) diff, err := sm.Diff(nil, terraform.NewResourceConfig(c), nil, nil)
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }

View File

@ -748,7 +748,7 @@ func (d *InstanceDiff) Same(d2 *InstanceDiff) (bool, string) {
delete(checkOld, k) delete(checkOld, k)
delete(checkNew, k) delete(checkNew, k)
_, ok := d2.GetAttribute(k) diffNew, ok := d2.GetAttribute(k)
if !ok { if !ok {
// If there's no new attribute, and the old diff expected the attribute // If there's no new attribute, and the old diff expected the attribute
// to be removed, that's just fine. // to be removed, that's just fine.
@ -837,7 +837,31 @@ func (d *InstanceDiff) Same(d2 *InstanceDiff) (bool, string) {
} }
} }
// TODO: check for the same value if not computed // If our attributes are not computed, then there is no reason why we can't
// check to make sure the diff values are the same. Do that now.
//
// There are several conditions that we need to pass here as they are
// allowed cases even if values don't match, so let's check those first.
switch {
case diffOld.NewComputed:
// NewComputed values pass
case strings.Contains(k, "~"):
// Computed keys for sets and lists
case strings.HasSuffix(k, "#"):
// Counts for sets need to be skipped as well as we have determined that
// we may not know the full value due to interpolation
case strings.HasSuffix(k, "%") && diffOld.New == "0" && diffOld.Old != "0":
// Lists can be skipped if they are being removed (going from n > 0 to 0)
case d.DestroyTainted && d2.GetDestroyTainted() && diffOld.New == diffNew.New:
// Same for DestoryTainted
case d.requiresNew() && d2.RequiresNew() && diffOld.New == diffNew.New:
// Same for RequiresNew
default:
// Anything that gets here should be able to be checked for deep equality.
if !reflect.DeepEqual(diffOld, diffNew) {
return false, fmt.Sprintf("value mismatch: %s", k)
}
}
} }
// Check for leftover attributes // Check for leftover attributes

View File

@ -1131,6 +1131,82 @@ func TestInstanceDiffSame(t *testing.T) {
true, true,
"", "",
}, },
{
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{
Old: "1",
New: "2",
},
},
},
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{
Old: "1",
New: "3",
},
},
},
false,
"value mismatch: foo",
},
// Make sure that DestroyTainted diffs pass as well, especially when diff
// two works off of no state.
{
&InstanceDiff{
DestroyTainted: true,
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{
Old: "foo",
New: "foo",
},
},
},
&InstanceDiff{
DestroyTainted: true,
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{
Old: "",
New: "foo",
},
},
},
true,
"",
},
// RequiresNew in different attribute
{
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{
Old: "foo",
New: "foo",
},
"bar": &ResourceAttrDiff{
Old: "bar",
New: "baz",
RequiresNew: true,
},
},
},
&InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{
Old: "",
New: "foo",
},
"bar": &ResourceAttrDiff{
Old: "",
New: "baz",
RequiresNew: true,
},
},
},
true,
"",
},
} }
for i, tc := range cases { for i, tc := range cases {