helper/schema: Add Computed to ResourceDiff, expose GetOkExists

This adds a new method to ResourceDiff: Computed, which exposes the
computed read result field to ResourceDiff. In the context of
customizing the diff, this is important as interpolated and otherwise
computed values will show up in the diff as blank, with no way of
determining if the value is actually blank or if it's a computed value
not available at diff customization time. Currently assumptions need to
be made on this, but this does not help in validation scenarios where
one needs to differentiate between an actual blank value and a value
that will be available later.

This is exposed for the most part via NewComputed in the diff, but the
tests cover both the config reader as well (with no diff, even though
this should not come up in normal operation) and also the newDiff reader
when someone sets a new value using SetNew and SetNewComputed.

This commit also exposes GetOkExists. The tests were mostly pulled from
ResourceData but a few were added to ensure that config was being
properly covered as well, in addition to covering SetNew and
SetNewComputed.
This commit is contained in:
Chris Marchesi 2018-03-21 14:15:06 -07:00
parent dfa623250a
commit 274b933077
No known key found for this signature in database
GPG Key ID: 8D6F1589D9834498
2 changed files with 777 additions and 0 deletions

View File

@ -378,6 +378,25 @@ func (d *ResourceDiff) GetOk(key string) (interface{}, bool) {
return r.Value, exists
}
// GetOkExists functions the same way as GetOkExists within ResourceData, but
// it also checks the new diff levels to provide data consistent with the
// current state of the customized diff.
func (d *ResourceDiff) GetOkExists(key string) (interface{}, bool) {
r := d.get(strings.Split(key, "."), "newDiff")
exists := r.Exists && !r.Computed
return r.Value, exists
}
// Computed exposes the computed value of a field read result to the caller.
// It's a function unique to ResourceDiff and allows users to check to see if a
// value is currently computed in the collective diff readers. No actual value
// is returned here as computed values are always blank until they are properly
// evaluated in the graph.
func (d *ResourceDiff) Computed(key string) bool {
r := d.get(strings.Split(key, "."), "newDiff")
return r.Computed
}
// 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 {

View File

@ -1,11 +1,14 @@
package schema
import (
"fmt"
"reflect"
"sort"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/hashicorp/hil/ast"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/terraform"
)
@ -1034,3 +1037,758 @@ func TestGetChangedKeysPrefix(t *testing.T) {
})
}
}
func TestResourceDiffGetOkExists(t *testing.T) {
cases := []struct {
Name string
Schema map[string]*Schema
State *terraform.InstanceState
Config *terraform.ResourceConfig
Diff *terraform.InstanceDiff
Key string
Value interface{}
Ok bool
}{
/*
* Primitives
*/
{
Name: "string-literal-empty",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
State: nil,
Config: nil,
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": {
Old: "",
New: "",
},
},
},
Key: "availability_zone",
Value: "",
Ok: true,
},
{
Name: "string-computed-empty",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
State: nil,
Config: nil,
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": {
Old: "",
New: "",
NewComputed: true,
},
},
},
Key: "availability_zone",
Value: "",
Ok: false,
},
{
Name: "string-optional-computed-nil-diff",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
State: nil,
Config: nil,
Diff: nil,
Key: "availability_zone",
Value: "",
Ok: false,
},
/*
* Lists
*/
{
Name: "list-optional",
Schema: map[string]*Schema{
"ports": {
Type: TypeList,
Optional: true,
Elem: &Schema{Type: TypeInt},
},
},
State: nil,
Config: nil,
Diff: nil,
Key: "ports",
Value: []interface{}{},
Ok: false,
},
/*
* Map
*/
{
Name: "map-optional",
Schema: map[string]*Schema{
"ports": {
Type: TypeMap,
Optional: true,
},
},
State: nil,
Config: nil,
Diff: nil,
Key: "ports",
Value: map[string]interface{}{},
Ok: false,
},
/*
* Set
*/
{
Name: "set-optional",
Schema: map[string]*Schema{
"ports": {
Type: TypeSet,
Optional: true,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int { return a.(int) },
},
},
State: nil,
Config: nil,
Diff: nil,
Key: "ports",
Value: []interface{}{},
Ok: false,
},
{
Name: "set-optional-key",
Schema: map[string]*Schema{
"ports": {
Type: TypeSet,
Optional: true,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int { return a.(int) },
},
},
State: nil,
Config: nil,
Diff: nil,
Key: "ports.0",
Value: 0,
Ok: false,
},
{
Name: "bool-literal-empty",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeBool,
Optional: true,
Computed: true,
ForceNew: true,
},
},
State: nil,
Config: nil,
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": {
Old: "",
New: "",
},
},
},
Key: "availability_zone",
Value: false,
Ok: true,
},
{
Name: "bool-literal-set",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeBool,
Optional: true,
Computed: true,
ForceNew: true,
},
},
State: nil,
Config: nil,
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": {
New: "true",
},
},
},
Key: "availability_zone",
Value: true,
Ok: true,
},
{
Name: "value-in-config",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"availability_zone": "foo",
},
},
Config: testConfig(t, map[string]interface{}{
"availability_zone": "foo",
}),
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{},
},
Key: "availability_zone",
Value: "foo",
Ok: true,
},
{
Name: "new-value-in-config",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
},
},
State: nil,
Config: testConfig(t, map[string]interface{}{
"availability_zone": "foo",
}),
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": {
Old: "",
New: "foo",
},
},
},
Key: "availability_zone",
Value: "foo",
Ok: true,
},
{
Name: "optional-computed-value-in-config",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"availability_zone": "foo",
},
},
Config: testConfig(t, map[string]interface{}{
"availability_zone": "bar",
}),
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": {
Old: "foo",
New: "bar",
},
},
},
Key: "availability_zone",
Value: "bar",
Ok: true,
},
{
Name: "removed-value",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"availability_zone": "foo",
},
},
Config: testConfig(t, map[string]interface{}{}),
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": {
Old: "foo",
New: "",
NewRemoved: true,
},
},
},
Key: "availability_zone",
Value: "",
Ok: true,
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
d := newResourceDiff(tc.Schema, tc.Config, tc.State, tc.Diff)
v, ok := d.GetOkExists(tc.Key)
if s, ok := v.(*Set); ok {
v = s.List()
}
if !reflect.DeepEqual(v, tc.Value) {
t.Fatalf("Bad %s: \n%#v", tc.Name, v)
}
if ok != tc.Ok {
t.Fatalf("%s: expected ok: %t, got: %t", tc.Name, tc.Ok, ok)
}
})
}
}
func TestResourceDiffGetOkExistsSetNew(t *testing.T) {
tc := struct {
Schema map[string]*Schema
State *terraform.InstanceState
Diff *terraform.InstanceDiff
Key string
Value interface{}
Ok bool
}{
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: nil,
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{},
},
Key: "availability_zone",
Value: "foobar",
Ok: true,
}
d := newResourceDiff(tc.Schema, testConfig(t, map[string]interface{}{}), tc.State, tc.Diff)
d.SetNew(tc.Key, tc.Value)
v, ok := d.GetOkExists(tc.Key)
if s, ok := v.(*Set); ok {
v = s.List()
}
if !reflect.DeepEqual(v, tc.Value) {
t.Fatalf("Bad: \n%#v", v)
}
if ok != tc.Ok {
t.Fatalf("expected ok: %t, got: %t", tc.Ok, ok)
}
}
func TestResourceDiffGetOkExistsSetNewComputed(t *testing.T) {
tc := struct {
Schema map[string]*Schema
State *terraform.InstanceState
Diff *terraform.InstanceDiff
Key string
Value interface{}
Ok bool
}{
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"availability_zone": "foo",
},
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{},
},
Key: "availability_zone",
Value: "foobar",
Ok: false,
}
d := newResourceDiff(tc.Schema, testConfig(t, map[string]interface{}{}), tc.State, tc.Diff)
d.SetNewComputed(tc.Key)
_, ok := d.GetOkExists(tc.Key)
if ok != tc.Ok {
t.Fatalf("expected ok: %t, got: %t", tc.Ok, ok)
}
}
func TestResourceDiffComputed(t *testing.T) {
cases := []struct {
Name string
Schema map[string]*Schema
State *terraform.InstanceState
Config *terraform.ResourceConfig
Diff *terraform.InstanceDiff
Key string
Expected bool
}{
{
Name: "in config, no state",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
},
},
State: nil,
Config: testConfig(t, map[string]interface{}{
"availability_zone": "foo",
}),
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": {
Old: "",
New: "foo",
},
},
},
Key: "availability_zone",
Expected: false,
},
{
Name: "in config, has state, no diff",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"availability_zone": "foo",
},
},
Config: testConfig(t, map[string]interface{}{
"availability_zone": "foo",
}),
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{},
},
Key: "availability_zone",
Expected: false,
},
{
Name: "computed attribute, in state, no diff",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Computed: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"availability_zone": "foo",
},
},
Config: testConfig(t, map[string]interface{}{}),
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{},
},
Key: "availability_zone",
Expected: false,
},
{
Name: "optional and computed attribute, in state, no config",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"availability_zone": "foo",
},
},
Config: testConfig(t, map[string]interface{}{}),
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{},
},
Key: "availability_zone",
Expected: false,
},
{
Name: "optional and computed attribute, in state, with config",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"availability_zone": "foo",
},
},
Config: testConfig(t, map[string]interface{}{
"availability_zone": "foo",
}),
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{},
},
Key: "availability_zone",
Expected: false,
},
{
Name: "computed value, through config reader",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"availability_zone": "foo",
},
},
Config: testConfigInterpolate(
t,
map[string]interface{}{
"availability_zone": "${var.foo}",
},
map[string]ast.Variable{
"var.foo": ast.Variable{
Value: config.UnknownVariableValue,
Type: ast.TypeString,
},
},
),
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{},
},
Key: "availability_zone",
Expected: true,
},
{
Name: "computed value, through diff reader",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"availability_zone": "foo",
},
},
Config: testConfigInterpolate(
t,
map[string]interface{}{
"availability_zone": "${var.foo}",
},
map[string]ast.Variable{
"var.foo": ast.Variable{
Value: config.UnknownVariableValue,
Type: ast.TypeString,
},
},
),
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": {
Old: "foo",
New: "",
NewComputed: true,
},
},
},
Key: "availability_zone",
Expected: true,
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
d := newResourceDiff(tc.Schema, tc.Config, tc.State, tc.Diff)
actual := d.Computed(tc.Key)
if tc.Expected != actual {
t.Fatalf("%s: expected ok: %t, got: %t", tc.Name, tc.Expected, actual)
}
})
}
}
func TestResourceDiffComputedSetNew(t *testing.T) {
tc := struct {
Schema map[string]*Schema
State *terraform.InstanceState
Config *terraform.ResourceConfig
Diff *terraform.InstanceDiff
Key string
Value interface{}
Expected bool
}{
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"availability_zone": "foo",
},
},
Config: testConfigInterpolate(
t,
map[string]interface{}{
"availability_zone": "${var.foo}",
},
map[string]ast.Variable{
"var.foo": ast.Variable{
Value: config.UnknownVariableValue,
Type: ast.TypeString,
},
},
),
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": {
Old: "foo",
New: "",
NewComputed: true,
},
},
},
Key: "availability_zone",
Value: "bar",
Expected: false,
}
d := newResourceDiff(tc.Schema, tc.Config, tc.State, tc.Diff)
d.SetNew(tc.Key, tc.Value)
actual := d.Computed(tc.Key)
if tc.Expected != actual {
t.Fatalf("expected ok: %t, got: %t", tc.Expected, actual)
}
}
func TestResourceDiffComputedSetNewComputed(t *testing.T) {
tc := struct {
Schema map[string]*Schema
State *terraform.InstanceState
Config *terraform.ResourceConfig
Diff *terraform.InstanceDiff
Key string
Expected bool
}{
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Computed: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"availability_zone": "foo",
},
},
Config: testConfig(t, map[string]interface{}{}),
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{},
},
Key: "availability_zone",
Expected: true,
}
d := newResourceDiff(tc.Schema, tc.Config, tc.State, tc.Diff)
d.SetNewComputed(tc.Key)
actual := d.Computed(tc.Key)
if tc.Expected != actual {
t.Fatalf("expected ok: %t, got: %t", tc.Expected, actual)
}
}