diff --git a/helper/customdiff/compose.go b/helper/customdiff/compose.go new file mode 100644 index 000000000..f7b31fb2c --- /dev/null +++ b/helper/customdiff/compose.go @@ -0,0 +1,72 @@ +package customdiff + +import ( + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform/helper/schema" +) + +// All returns a CustomizeDiffFunc that runs all of the given +// CustomizeDiffFuncs and returns all of the errors produced. +// +// If one function produces an error, functions after it are still run. +// If this is not desirable, use function Sequence instead. +// +// If multiple functions returns errors, the result is a multierror. +// +// For example: +// +// &schema.Resource{ +// // ... +// CustomizeDiff: customdiff.All( +// customdiff.ValidateChange("size", func (old, new, meta interface{}) error { +// // If we are increasing "size" then the new value must be +// // a multiple of the old value. +// if new.(int) <= old.(int) { +// return nil +// } +// if (new.(int) % old.(int)) != 0 { +// return fmt.Errorf("new size value must be an integer multiple of old value %d", old.(int)) +// } +// return nil +// }), +// customdiff.ForceNewIfChange("size", func (old, new, meta interface{}) bool { +// // "size" can only increase in-place, so we must create a new resource +// // if it is decreased. +// return new.(int) < old.(int) +// }), +// customdiff.ComputedIf("version_id", func (d *schema.ResourceDiff, meta interface{}) bool { +// // Any change to "content" causes a new "version_id" to be allocated. +// return d.HasChange("content") +// }), +// ), +// } +// +func All(funcs ...schema.CustomizeDiffFunc) schema.CustomizeDiffFunc { + return func(d *schema.ResourceDiff, meta interface{}) error { + var err error + for _, f := range funcs { + thisErr := f(d, meta) + if thisErr != nil { + err = multierror.Append(err, thisErr) + } + } + return err + } +} + +// Sequence returns a CustomizeDiffFunc that runs all of the given +// CustomizeDiffFuncs in sequence, stopping at the first one that returns +// an error and returning that error. +// +// If all functions succeed, the combined function also succeeds. +func Sequence(funcs ...schema.CustomizeDiffFunc) schema.CustomizeDiffFunc { + return func(d *schema.ResourceDiff, meta interface{}) error { + for _, f := range funcs { + err := f(d, meta) + if err != nil { + return err + } + } + return nil + } +} diff --git a/helper/customdiff/compose_test.go b/helper/customdiff/compose_test.go new file mode 100644 index 000000000..2f1e891f3 --- /dev/null +++ b/helper/customdiff/compose_test.go @@ -0,0 +1,110 @@ +package customdiff + +import ( + "errors" + "strings" + "testing" + + "github.com/hashicorp/terraform/helper/schema" +) + +func TestAll(t *testing.T) { + var aCalled, bCalled, cCalled bool + + provider := testProvider( + map[string]*schema.Schema{}, + All( + func(d *schema.ResourceDiff, meta interface{}) error { + aCalled = true + return errors.New("A bad") + }, + func(d *schema.ResourceDiff, meta interface{}) error { + bCalled = true + return nil + }, + func(d *schema.ResourceDiff, meta interface{}) error { + cCalled = true + return errors.New("C bad") + }, + ), + ) + + _, err := testDiff( + provider, + map[string]string{ + "foo": "bar", + }, + map[string]string{ + "foo": "baz", + }, + ) + + if err == nil { + t.Fatal("Diff succeeded; want error") + } + if s, sub := err.Error(), "* A bad"; !strings.Contains(s, sub) { + t.Errorf("Missing substring %q in error message %q", sub, s) + } + if s, sub := err.Error(), "* C bad"; !strings.Contains(s, sub) { + t.Errorf("Missing substring %q in error message %q", sub, s) + } + + if !aCalled { + t.Error("customize callback A was not called") + } + if !bCalled { + t.Error("customize callback B was not called") + } + if !cCalled { + t.Error("customize callback C was not called") + } +} + +func TestSequence(t *testing.T) { + var aCalled, bCalled, cCalled bool + + provider := testProvider( + map[string]*schema.Schema{}, + Sequence( + func(d *schema.ResourceDiff, meta interface{}) error { + aCalled = true + return nil + }, + func(d *schema.ResourceDiff, meta interface{}) error { + bCalled = true + return errors.New("B bad") + }, + func(d *schema.ResourceDiff, meta interface{}) error { + cCalled = true + return errors.New("C bad") + }, + ), + ) + + _, err := testDiff( + provider, + map[string]string{ + "foo": "bar", + }, + map[string]string{ + "foo": "baz", + }, + ) + + if err == nil { + t.Fatal("Diff succeeded; want error") + } + if got, want := err.Error(), "B bad"; got != want { + t.Errorf("Wrong error message %q; want %q", got, want) + } + + if !aCalled { + t.Error("customize callback A was not called") + } + if !bCalled { + t.Error("customize callback B was not called") + } + if cCalled { + t.Error("customize callback C was called (should not have been)") + } +} diff --git a/helper/customdiff/computed.go b/helper/customdiff/computed.go new file mode 100644 index 000000000..a6fa1a827 --- /dev/null +++ b/helper/customdiff/computed.go @@ -0,0 +1,16 @@ +package customdiff + +import ( + "github.com/hashicorp/terraform/helper/schema" +) + +// ComputedIf returns a CustomizeDiffFunc that sets the given key's new value +// as computed if the given condition function returns true. +func ComputedIf(key string, f ResourceConditionFunc) schema.CustomizeDiffFunc { + return func(d *schema.ResourceDiff, meta interface{}) error { + if f(d, meta) { + d.SetNewComputed(key) + } + return nil + } +} diff --git a/helper/customdiff/computed_test.go b/helper/customdiff/computed_test.go new file mode 100644 index 000000000..4fa3e0b1b --- /dev/null +++ b/helper/customdiff/computed_test.go @@ -0,0 +1,126 @@ +package customdiff + +import ( + "testing" + + "github.com/hashicorp/terraform/helper/schema" +) + +func TestComputedIf(t *testing.T) { + t.Run("true", func(t *testing.T) { + var condCalls int + var gotOld, gotNew string + + provider := testProvider( + map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + Optional: true, + }, + "comp": { + Type: schema.TypeString, + Computed: true, + }, + }, + ComputedIf("comp", func(d *schema.ResourceDiff, meta interface{}) bool { + // When we set "ForceNew", our CustomizeDiff function is actually + // called a second time to construct the "create" portion of + // the replace diff. On the second call, the old value is masked + // as "" to suggest that the object is being created rather than + // updated. + + condCalls++ + old, new := d.GetChange("foo") + gotOld = old.(string) + gotNew = new.(string) + + return true + }), + ) + + diff, err := testDiff( + provider, + map[string]string{ + "foo": "bar", + "comp": "old", + }, + map[string]string{ + "foo": "baz", + }, + ) + + if err != nil { + t.Fatalf("Diff failed with error: %s", err) + } + + if condCalls != 1 { + t.Fatalf("Wrong number of conditional callback calls %d; want %d", condCalls, 1) + } else { + if got, want := gotOld, "bar"; got != want { + t.Errorf("wrong old value %q on first call; want %q", got, want) + } + if got, want := gotNew, "baz"; got != want { + t.Errorf("wrong new value %q on first call; want %q", got, want) + } + } + + if !diff.Attributes["comp"].NewComputed { + t.Error("Attribute 'comp' is not marked as NewComputed") + } + }) + t.Run("false", func(t *testing.T) { + var condCalls int + var gotOld, gotNew string + + provider := testProvider( + map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + Optional: true, + }, + "comp": { + Type: schema.TypeString, + Computed: true, + }, + }, + ComputedIf("comp", func(d *schema.ResourceDiff, meta interface{}) bool { + condCalls++ + old, new := d.GetChange("foo") + gotOld = old.(string) + gotNew = new.(string) + + return false + }), + ) + + diff, err := testDiff( + provider, + map[string]string{ + "foo": "bar", + "comp": "old", + }, + map[string]string{ + "foo": "baz", + }, + ) + + if err != nil { + t.Fatalf("Diff failed with error: %s", err) + } + + if condCalls != 1 { + t.Fatalf("Wrong number of conditional callback calls %d; want %d", condCalls, 1) + } else { + if got, want := gotOld, "bar"; got != want { + t.Errorf("wrong old value %q on first call; want %q", got, want) + } + if got, want := gotNew, "baz"; got != want { + t.Errorf("wrong new value %q on first call; want %q", got, want) + } + } + + if diff.Attributes["comp"] != nil && diff.Attributes["comp"].NewComputed { + t.Error("Attribute 'foo' is marked as NewComputed, but should not be") + } + }) +} diff --git a/helper/customdiff/condition.go b/helper/customdiff/condition.go new file mode 100644 index 000000000..2271c4027 --- /dev/null +++ b/helper/customdiff/condition.go @@ -0,0 +1,60 @@ +package customdiff + +import ( + "github.com/hashicorp/terraform/helper/schema" +) + +// ResourceConditionFunc is a function type that makes a boolean decision based +// on an entire resource diff. +type ResourceConditionFunc func(d *schema.ResourceDiff, meta interface{}) bool + +// ValueChangeConditionFunc is a function type that makes a boolean decision +// by comparing two values. +type ValueChangeConditionFunc func(old, new, meta interface{}) bool + +// ValueConditionFunc is a function type that makes a boolean decision based +// on a given value. +type ValueConditionFunc func(value, meta interface{}) bool + +// If returns a CustomizeDiffFunc that calls the given condition +// function and then calls the given CustomizeDiffFunc only if the condition +// function returns true. +// +// This can be used to include conditional customizations when composing +// customizations using All and Sequence, but should generally be used only in +// simple scenarios. Prefer directly writing a CustomizeDiffFunc containing +// a conditional branch if the given CustomizeDiffFunc is already a +// locally-defined function, since this avoids obscuring the control flow. +func If(cond ResourceConditionFunc, f schema.CustomizeDiffFunc) schema.CustomizeDiffFunc { + return func(d *schema.ResourceDiff, meta interface{}) error { + if cond(d, meta) { + return f(d, meta) + } + return nil + } +} + +// IfValueChange returns a CustomizeDiffFunc that calls the given condition +// function with the old and new values of the given key and then calls the +// given CustomizeDiffFunc only if the condition function returns true. +func IfValueChange(key string, cond ValueChangeConditionFunc, f schema.CustomizeDiffFunc) schema.CustomizeDiffFunc { + return func(d *schema.ResourceDiff, meta interface{}) error { + old, new := d.GetChange(key) + if cond(old, new, meta) { + return f(d, meta) + } + return nil + } +} + +// IfValue returns a CustomizeDiffFunc that calls the given condition +// function with the new values of the given key and then calls the +// given CustomizeDiffFunc only if the condition function returns true. +func IfValue(key string, cond ValueConditionFunc, f schema.CustomizeDiffFunc) schema.CustomizeDiffFunc { + return func(d *schema.ResourceDiff, meta interface{}) error { + if cond(d.Get(key), meta) { + return f(d, meta) + } + return nil + } +} diff --git a/helper/customdiff/condition_test.go b/helper/customdiff/condition_test.go new file mode 100644 index 000000000..717bc7f32 --- /dev/null +++ b/helper/customdiff/condition_test.go @@ -0,0 +1,348 @@ +package customdiff + +import ( + "errors" + "testing" + + "github.com/hashicorp/terraform/helper/schema" +) + +func TestIf(t *testing.T) { + t.Run("true", func(t *testing.T) { + var condCalled, customCalled bool + var gotOld, gotNew string + + provider := testProvider( + map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + Optional: true, + }, + }, + If( + func(d *schema.ResourceDiff, meta interface{}) bool { + condCalled = true + old, new := d.GetChange("foo") + gotOld = old.(string) + gotNew = new.(string) + return true + }, + func(d *schema.ResourceDiff, meta interface{}) error { + customCalled = true + return errors.New("bad") + }, + ), + ) + + _, err := testDiff( + provider, + map[string]string{ + "foo": "bar", + }, + map[string]string{ + "foo": "baz", + }, + ) + + if err == nil { + t.Fatal("Diff succeeded; want error") + } + if got, want := err.Error(), "bad"; got != want { + t.Fatalf("wrong error message %q; want %q", got, want) + } + + if !condCalled { + t.Error("condition callback was not called") + } else { + if got, want := gotOld, "bar"; got != want { + t.Errorf("wrong old value %q; want %q", got, want) + } + if got, want := gotNew, "baz"; got != want { + t.Errorf("wrong new value %q; want %q", got, want) + } + } + + if !customCalled { + t.Error("customize callback was not called") + } + }) + t.Run("false", func(t *testing.T) { + var condCalled, customCalled bool + var gotOld, gotNew string + + provider := testProvider( + map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + Optional: true, + }, + }, + If( + func(d *schema.ResourceDiff, meta interface{}) bool { + condCalled = true + old, new := d.GetChange("foo") + gotOld = old.(string) + gotNew = new.(string) + return false + }, + func(d *schema.ResourceDiff, meta interface{}) error { + customCalled = true + return errors.New("bad") + }, + ), + ) + + _, err := testDiff( + provider, + map[string]string{ + "foo": "bar", + }, + map[string]string{ + "foo": "baz", + }, + ) + + if err != nil { + t.Fatalf("Diff error %q; want success", err.Error()) + } + + if !condCalled { + t.Error("condition callback was not called") + } else { + if got, want := gotOld, "bar"; got != want { + t.Errorf("wrong old value %q; want %q", got, want) + } + if got, want := gotNew, "baz"; got != want { + t.Errorf("wrong new value %q; want %q", got, want) + } + } + + if customCalled { + t.Error("customize callback was called (should not have been)") + } + }) +} + +func TestIfValueChange(t *testing.T) { + t.Run("true", func(t *testing.T) { + var condCalled, customCalled bool + var gotOld, gotNew string + + provider := testProvider( + map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + Optional: true, + }, + }, + IfValueChange( + "foo", + func(old, new, meta interface{}) bool { + condCalled = true + gotOld = old.(string) + gotNew = new.(string) + return true + }, + func(d *schema.ResourceDiff, meta interface{}) error { + customCalled = true + return errors.New("bad") + }, + ), + ) + + _, err := testDiff( + provider, + map[string]string{ + "foo": "bar", + }, + map[string]string{ + "foo": "baz", + }, + ) + + if err == nil { + t.Fatal("Diff succeeded; want error") + } + if got, want := err.Error(), "bad"; got != want { + t.Fatalf("wrong error message %q; want %q", got, want) + } + + if !condCalled { + t.Error("condition callback was not called") + } else { + if got, want := gotOld, "bar"; got != want { + t.Errorf("wrong old value %q; want %q", got, want) + } + if got, want := gotNew, "baz"; got != want { + t.Errorf("wrong new value %q; want %q", got, want) + } + } + + if !customCalled { + t.Error("customize callback was not called") + } + }) + t.Run("false", func(t *testing.T) { + var condCalled, customCalled bool + var gotOld, gotNew string + + provider := testProvider( + map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + Optional: true, + }, + }, + IfValueChange( + "foo", + func(old, new, meta interface{}) bool { + condCalled = true + gotOld = old.(string) + gotNew = new.(string) + return false + }, + func(d *schema.ResourceDiff, meta interface{}) error { + customCalled = true + return errors.New("bad") + }, + ), + ) + + _, err := testDiff( + provider, + map[string]string{ + "foo": "bar", + }, + map[string]string{ + "foo": "baz", + }, + ) + + if err != nil { + t.Fatalf("Diff error %q; want success", err.Error()) + } + + if !condCalled { + t.Error("condition callback was not called") + } else { + if got, want := gotOld, "bar"; got != want { + t.Errorf("wrong old value %q; want %q", got, want) + } + if got, want := gotNew, "baz"; got != want { + t.Errorf("wrong new value %q; want %q", got, want) + } + } + + if customCalled { + t.Error("customize callback was called (should not have been)") + } + }) +} + +func TestIfValue(t *testing.T) { + t.Run("true", func(t *testing.T) { + var condCalled, customCalled bool + var gotValue string + + provider := testProvider( + map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + Optional: true, + }, + }, + IfValue( + "foo", + func(value, meta interface{}) bool { + condCalled = true + gotValue = value.(string) + return true + }, + func(d *schema.ResourceDiff, meta interface{}) error { + customCalled = true + return errors.New("bad") + }, + ), + ) + + _, err := testDiff( + provider, + map[string]string{ + "foo": "bar", + }, + map[string]string{ + "foo": "baz", + }, + ) + + if err == nil { + t.Fatal("Diff succeeded; want error") + } + if got, want := err.Error(), "bad"; got != want { + t.Fatalf("wrong error message %q; want %q", got, want) + } + + if !condCalled { + t.Error("condition callback was not called") + } else { + if got, want := gotValue, "baz"; got != want { + t.Errorf("wrong value %q; want %q", got, want) + } + } + + if !customCalled { + t.Error("customize callback was not called") + } + }) + t.Run("false", func(t *testing.T) { + var condCalled, customCalled bool + var gotValue string + + provider := testProvider( + map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + Optional: true, + }, + }, + IfValue( + "foo", + func(value, meta interface{}) bool { + condCalled = true + gotValue = value.(string) + return false + }, + func(d *schema.ResourceDiff, meta interface{}) error { + customCalled = true + return errors.New("bad") + }, + ), + ) + + _, err := testDiff( + provider, + map[string]string{ + "foo": "bar", + }, + map[string]string{ + "foo": "baz", + }, + ) + + if err != nil { + t.Fatalf("Diff error %q; want success", err.Error()) + } + + if !condCalled { + t.Error("condition callback was not called") + } else { + if got, want := gotValue, "baz"; got != want { + t.Errorf("wrong value %q; want %q", got, want) + } + } + + if customCalled { + t.Error("customize callback was called (should not have been)") + } + }) +} diff --git a/helper/customdiff/doc.go b/helper/customdiff/doc.go new file mode 100644 index 000000000..c6ad1199c --- /dev/null +++ b/helper/customdiff/doc.go @@ -0,0 +1,11 @@ +// Package customdiff provides a set of reusable and composable functions +// to enable more "declarative" use of the CustomizeDiff mechanism available +// for resources in package helper/schema. +// +// The intent of these helpers is to make the intent of a set of diff +// customizations easier to see, rather than lost in a sea of Go function +// boilerplate. They should _not_ be used in situations where they _obscure_ +// intent, e.g. by over-using the composition functions where a single +// function containing normal Go control flow statements would be more +// straightforward. +package customdiff diff --git a/helper/customdiff/force_new.go b/helper/customdiff/force_new.go new file mode 100644 index 000000000..29e02c988 --- /dev/null +++ b/helper/customdiff/force_new.go @@ -0,0 +1,40 @@ +package customdiff + +import ( + "github.com/hashicorp/terraform/helper/schema" +) + +// ForceNewIf returns a CustomizeDiffFunc that flags the given key as +// requiring a new resource if the given condition function returns true. +// +// The return value of the condition function is ignored if the old and new +// values of the field compare equal, since no attribute diff is generated in +// that case. +func ForceNewIf(key string, f ResourceConditionFunc) schema.CustomizeDiffFunc { + return func(d *schema.ResourceDiff, meta interface{}) error { + if f(d, meta) { + d.ForceNew(key) + } + return nil + } +} + +// ForceNewIfChange returns a CustomizeDiffFunc that flags the given key as +// requiring a new resource if the given condition function returns true. +// +// The return value of the condition function is ignored if the old and new +// values compare equal, since no attribute diff is generated in that case. +// +// This function is similar to ForceNewIf but provides the condition function +// only the old and new values of the given key, which leads to more compact +// and explicit code in the common case where the decision can be made with +// only the specific field value. +func ForceNewIfChange(key string, f ValueChangeConditionFunc) schema.CustomizeDiffFunc { + return func(d *schema.ResourceDiff, meta interface{}) error { + old, new := d.GetChange(key) + if f(old, new, meta) { + d.ForceNew(key) + } + return nil + } +} diff --git a/helper/customdiff/force_new_test.go b/helper/customdiff/force_new_test.go new file mode 100644 index 000000000..859a76151 --- /dev/null +++ b/helper/customdiff/force_new_test.go @@ -0,0 +1,249 @@ +package customdiff + +import ( + "testing" + + "github.com/hashicorp/terraform/helper/schema" +) + +func TestForceNewIf(t *testing.T) { + t.Run("true", func(t *testing.T) { + var condCalls int + var gotOld1, gotNew1, gotOld2, gotNew2 string + + provider := testProvider( + map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + Optional: true, + }, + }, + ForceNewIf("foo", func(d *schema.ResourceDiff, meta interface{}) bool { + // When we set "ForceNew", our CustomizeDiff function is actually + // called a second time to construct the "create" portion of + // the replace diff. On the second call, the old value is masked + // as "" to suggest that the object is being created rather than + // updated. + + condCalls++ + old, new := d.GetChange("foo") + + switch condCalls { + case 1: + gotOld1 = old.(string) + gotNew1 = new.(string) + case 2: + gotOld2 = old.(string) + gotNew2 = new.(string) + } + + return true + }), + ) + + diff, err := testDiff( + provider, + map[string]string{ + "foo": "bar", + }, + map[string]string{ + "foo": "baz", + }, + ) + + if err != nil { + t.Fatalf("Diff failed with error: %s", err) + } + + if condCalls != 2 { + t.Fatalf("Wrong number of conditional callback calls %d; want %d", condCalls, 2) + } else { + if got, want := gotOld1, "bar"; got != want { + t.Errorf("wrong old value %q on first call; want %q", got, want) + } + if got, want := gotNew1, "baz"; got != want { + t.Errorf("wrong new value %q on first call; want %q", got, want) + } + if got, want := gotOld2, ""; got != want { + t.Errorf("wrong old value %q on first call; want %q", got, want) + } + if got, want := gotNew2, "baz"; got != want { + t.Errorf("wrong new value %q on first call; want %q", got, want) + } + } + + if !diff.Attributes["foo"].RequiresNew { + t.Error("Attribute 'foo' is not marked as RequiresNew") + } + }) + t.Run("false", func(t *testing.T) { + var condCalls int + var gotOld, gotNew string + + provider := testProvider( + map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + Optional: true, + }, + }, + ForceNewIf("foo", func(d *schema.ResourceDiff, meta interface{}) bool { + condCalls++ + old, new := d.GetChange("foo") + gotOld = old.(string) + gotNew = new.(string) + + return false + }), + ) + + diff, err := testDiff( + provider, + map[string]string{ + "foo": "bar", + }, + map[string]string{ + "foo": "baz", + }, + ) + + if err != nil { + t.Fatalf("Diff failed with error: %s", err) + } + + if condCalls != 1 { + t.Fatalf("Wrong number of conditional callback calls %d; want %d", condCalls, 1) + } else { + if got, want := gotOld, "bar"; got != want { + t.Errorf("wrong old value %q on first call; want %q", got, want) + } + if got, want := gotNew, "baz"; got != want { + t.Errorf("wrong new value %q on first call; want %q", got, want) + } + } + + if diff.Attributes["foo"].RequiresNew { + t.Error("Attribute 'foo' is marked as RequiresNew, but should not be") + } + }) +} + +func TestForceNewIfChange(t *testing.T) { + t.Run("true", func(t *testing.T) { + var condCalls int + var gotOld1, gotNew1, gotOld2, gotNew2 string + + provider := testProvider( + map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + Optional: true, + }, + }, + ForceNewIfChange("foo", func(old, new, meta interface{}) bool { + // When we set "ForceNew", our CustomizeDiff function is actually + // called a second time to construct the "create" portion of + // the replace diff. On the second call, the old value is masked + // as "" to suggest that the object is being created rather than + // updated. + + condCalls++ + + switch condCalls { + case 1: + gotOld1 = old.(string) + gotNew1 = new.(string) + case 2: + gotOld2 = old.(string) + gotNew2 = new.(string) + } + + return true + }), + ) + + diff, err := testDiff( + provider, + map[string]string{ + "foo": "bar", + }, + map[string]string{ + "foo": "baz", + }, + ) + + if err != nil { + t.Fatalf("Diff failed with error: %s", err) + } + + if condCalls != 2 { + t.Fatalf("Wrong number of conditional callback calls %d; want %d", condCalls, 2) + } else { + if got, want := gotOld1, "bar"; got != want { + t.Errorf("wrong old value %q on first call; want %q", got, want) + } + if got, want := gotNew1, "baz"; got != want { + t.Errorf("wrong new value %q on first call; want %q", got, want) + } + if got, want := gotOld2, ""; got != want { + t.Errorf("wrong old value %q on first call; want %q", got, want) + } + if got, want := gotNew2, "baz"; got != want { + t.Errorf("wrong new value %q on first call; want %q", got, want) + } + } + + if !diff.Attributes["foo"].RequiresNew { + t.Error("Attribute 'foo' is not marked as RequiresNew") + } + }) + t.Run("false", func(t *testing.T) { + var condCalls int + var gotOld, gotNew string + + provider := testProvider( + map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + Optional: true, + }, + }, + ForceNewIfChange("foo", func(old, new, meta interface{}) bool { + condCalls++ + gotOld = old.(string) + gotNew = new.(string) + + return false + }), + ) + + diff, err := testDiff( + provider, + map[string]string{ + "foo": "bar", + }, + map[string]string{ + "foo": "baz", + }, + ) + + if err != nil { + t.Fatalf("Diff failed with error: %s", err) + } + + if condCalls != 1 { + t.Fatalf("Wrong number of conditional callback calls %d; want %d", condCalls, 1) + } else { + if got, want := gotOld, "bar"; got != want { + t.Errorf("wrong old value %q on first call; want %q", got, want) + } + if got, want := gotNew, "baz"; got != want { + t.Errorf("wrong new value %q on first call; want %q", got, want) + } + } + + if diff.Attributes["foo"].RequiresNew { + t.Error("Attribute 'foo' is marked as RequiresNew, but should not be") + } + }) +} diff --git a/helper/customdiff/testing_test.go b/helper/customdiff/testing_test.go new file mode 100644 index 000000000..934113985 --- /dev/null +++ b/helper/customdiff/testing_test.go @@ -0,0 +1,38 @@ +package customdiff + +import ( + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +func testProvider(s map[string]*schema.Schema, cd schema.CustomizeDiffFunc) terraform.ResourceProvider { + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "test": { + Schema: s, + CustomizeDiff: cd, + }, + }, + } +} + +func testDiff(provider terraform.ResourceProvider, old, new map[string]string) (*terraform.InstanceDiff, error) { + newI := make(map[string]interface{}, len(new)) + for k, v := range new { + newI[k] = v + } + + return provider.Diff( + &terraform.InstanceInfo{ + Id: "test", + Type: "test", + ModulePath: []string{}, + }, + &terraform.InstanceState{ + Attributes: old, + }, + &terraform.ResourceConfig{ + Config: newI, + }, + ) +} diff --git a/helper/customdiff/validate.go b/helper/customdiff/validate.go new file mode 100644 index 000000000..bdf46f5f0 --- /dev/null +++ b/helper/customdiff/validate.go @@ -0,0 +1,38 @@ +package customdiff + +import ( + "github.com/hashicorp/terraform/helper/schema" +) + +// ValueChangeValidationFunc is a function type that validates the difference +// (or lack thereof) between two values, returning an error if the change +// is invalid. +type ValueChangeValidationFunc func(old, new, meta interface{}) error + +// ValueValidationFunc is a function type that validates a particular value, +// returning an error if the value is invalid. +type ValueValidationFunc func(value, meta interface{}) error + +// ValidateChange returns a CustomizeDiffFunc that applies the given validation +// function to the change for the given key, returning any error produced. +func ValidateChange(key string, f ValueChangeValidationFunc) schema.CustomizeDiffFunc { + return func(d *schema.ResourceDiff, meta interface{}) error { + old, new := d.GetChange(key) + return f(old, new, meta) + } +} + +// ValidateValue returns a CustomizeDiffFunc that applies the given validation +// function to value of the given key, returning any error produced. +// +// This should generally not be used since it is functionally equivalent to +// a validation function applied directly to the schema attribute in question, +// but is provided for situations where composing multiple CustomizeDiffFuncs +// together makes intent clearer than spreading that validation across the +// schema. +func ValidateValue(key string, f ValueValidationFunc) schema.CustomizeDiffFunc { + return func(d *schema.ResourceDiff, meta interface{}) error { + val := d.Get(key) + return f(val, meta) + } +} diff --git a/helper/customdiff/validate_test.go b/helper/customdiff/validate_test.go new file mode 100644 index 000000000..b51525988 --- /dev/null +++ b/helper/customdiff/validate_test.go @@ -0,0 +1,98 @@ +package customdiff + +import ( + "errors" + "testing" + + "github.com/hashicorp/terraform/helper/schema" +) + +func TestValidateChange(t *testing.T) { + var called bool + var gotOld, gotNew string + + provider := testProvider( + map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + Optional: true, + }, + }, + ValidateChange("foo", func(old, new, meta interface{}) error { + called = true + gotOld = old.(string) + gotNew = new.(string) + return errors.New("bad") + }), + ) + + _, err := testDiff( + provider, + map[string]string{ + "foo": "bar", + }, + map[string]string{ + "foo": "baz", + }, + ) + + if err == nil { + t.Fatalf("Diff succeeded; want error") + } + if got, want := err.Error(), "bad"; got != want { + t.Fatalf("wrong error message %q; want %q", got, want) + } + + if !called { + t.Fatal("ValidateChange callback was not called") + } + if got, want := gotOld, "bar"; got != want { + t.Errorf("wrong old value %q; want %q", got, want) + } + if got, want := gotNew, "baz"; got != want { + t.Errorf("wrong new value %q; want %q", got, want) + } +} + +func TestValidateValue(t *testing.T) { + var called bool + var gotValue string + + provider := testProvider( + map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + Optional: true, + }, + }, + ValidateValue("foo", func(value, meta interface{}) error { + called = true + gotValue = value.(string) + return errors.New("bad") + }), + ) + + _, err := testDiff( + provider, + map[string]string{ + "foo": "bar", + }, + map[string]string{ + "foo": "baz", + }, + ) + + if err == nil { + t.Fatalf("Diff succeeded; want error") + } + if got, want := err.Error(), "bad"; got != want { + t.Fatalf("wrong error message %q; want %q", got, want) + } + + if !called { + t.Fatal("ValidateValue callback was not called") + } + if got, want := gotValue, "baz"; got != want { + t.Errorf("wrong value %q; want %q", got, want) + } +}