helper/customdiff: Helper functions for CustomizeDiff
The CustomizeDiff functionality in helper/schema is powerful, but directly writing single CustomizeDiff functions can obscure the intent when a number of different, orthogonal diff-customization behaviors are required. This new library provides some building blocks that aim to allow a more declarative form of CustomizeDiff implementation, by composing a number of smaller operations. 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") }), ), } The goal is to allow the various separate operations to be quickly seen and to ensure that each of them runs independently of the others. These functions all create closures on the call parameters, so the result is still just a normal CustomizeDiffFunc and so the helpers in this package can be combined with hand-written functions as needed. As we get more experience writing CustomizeDiff functions we may wish to expand the repertoire of functions here in future; this initial set attempts to cover some common cases we've seen so far. We may also investigate some helper functions that are entirely declarative and so don't take callback functions at all, but want to learn what the relevant use-cases are before going in too deep here.
This commit is contained in:
parent
1d1984771b
commit
c647b22d97
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)")
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue