Merge #14887: Custom diff logic for providers
This commit is contained in:
commit
4787428cad
|
@ -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(),
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue