terraform: ResourceConfig reimplementation for TypeUnknown

The primary change here is to expect that Config contains computed
values. This introduces `unknownCheckWalker` that does a really basic
reflectwalk to look for computed values and use that for IsComputed.

We had a weird mixture before checking whether c.Config was simply
missing values to determine where to look. Now we rely on IsComputed
heavily.
This commit is contained in:
Mitchell Hashimoto 2016-11-09 14:15:38 -08:00
parent 59bd1a22f4
commit 9b5b122f14
No known key found for this signature in database
GPG Key ID: 744E147AA52F5B0A
4 changed files with 214 additions and 263 deletions

View File

@ -24,9 +24,7 @@ func TestEvalValidateResource_managedResource(t *testing.T) {
}
p := ResourceProvider(mp)
rc := &ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
rc := testResourceConfig(t, map[string]interface{}{"foo": "bar"})
node := &EvalValidateResource{
Provider: &p,
Config: &rc,
@ -61,9 +59,7 @@ func TestEvalValidateResource_dataSource(t *testing.T) {
}
p := ResourceProvider(mp)
rc := &ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
rc := testResourceConfig(t, map[string]interface{}{"foo": "bar"})
node := &EvalValidateResource{
Provider: &p,
Config: &rc,

View File

@ -9,6 +9,7 @@ import (
"github.com/hashicorp/terraform/config"
"github.com/mitchellh/copystructure"
"github.com/mitchellh/reflectwalk"
)
// ResourceProvisionerConfig is used to pair a provisioner
@ -186,16 +187,17 @@ func (c *ResourceConfig) CheckSet(keys []string) []error {
// Get looks up a configuration value by key and returns the value.
//
// The second return value is true if the get was successful. Get will
// not succeed if the value is being computed.
// return the raw value if the key is computed, so you should pair this
// with IsComputed.
func (c *ResourceConfig) Get(k string) (interface{}, bool) {
// First try to get it from c.Config since that has interpolated values
result, ok := c.get(k, c.Config)
if ok {
return result, ok
// We aim to get a value from the configuration. If it is computed,
// then we return the pure raw value.
source := c.Config
if c.IsComputed(k) {
source = c.Raw
}
// Otherwise, just get it from the raw config
return c.get(k, c.Raw)
return c.get(k, source)
}
// GetRaw looks up a configuration value by key and returns the value,
@ -209,22 +211,25 @@ func (c *ResourceConfig) GetRaw(k string) (interface{}, bool) {
// IsComputed returns whether the given key is computed or not.
func (c *ResourceConfig) IsComputed(k string) bool {
// First, check for pure computed key equality since that is fast
for _, ck := range c.ComputedKeys {
if ck == k {
return true
}
}
// The next thing we do is check the config if we get a computed
// value out of it.
v, ok := c.get(k, c.Config)
_, okRaw := c.get(k, c.Raw)
if !ok {
return false
}
// Both tests probably aren't needed anymore since we don't remove
// values any longer. The latter is probably good enough since we
// thread through that value now.
return (!ok && okRaw) || v == config.UnknownVariableValue
// If value is nil, then it isn't computed
if v == nil {
return false
}
// Test if the value contains an unknown value
var w unknownCheckWalker
if err := reflectwalk.Walk(v, &w); err != nil {
panic(err)
}
return w.Unknown
}
// IsSet checks if the key in the configuration is set. A key is set if
@ -238,11 +243,9 @@ func (c *ResourceConfig) IsSet(k string) bool {
return false
}
for _, ck := range c.ComputedKeys {
if ck == k {
if c.IsComputed(k) {
return true
}
}
if _, ok := c.Get(k); ok {
return true
@ -261,7 +264,6 @@ func (c *ResourceConfig) get(
var current interface{} = raw
var previous interface{} = nil
for i, part := range parts {
println(fmt.Sprintf("%#v: %#v %T", part, current, current))
if current == nil {
return nil, false
}
@ -289,15 +291,15 @@ func (c *ResourceConfig) get(
case reflect.Slice:
previous = current
if part == "#" {
// If any value in a list is computed, this whole thing
// is computed and we can't read any part of it.
for i := 0; i < cv.Len(); i++ {
if v := cv.Index(i).Interface(); v == unknownValue() {
return v, false
return v, true
}
}
if part == "#" {
current = cv.Len()
} else {
i, err := strconv.ParseInt(part, 0, 0)
@ -310,11 +312,6 @@ func (c *ResourceConfig) get(
current = cv.Index(int(i)).Interface()
}
case reflect.String:
// If we get the unknown value, return that
if current == unknownValue() {
return current, false
}
// This happens when map keys contain "." and have a common
// prefix so were split as path components above.
actualKey := strings.Join(parts[i-1:], ".")
@ -328,23 +325,6 @@ func (c *ResourceConfig) get(
}
}
switch cv := reflect.ValueOf(current); cv.Kind() {
case reflect.Slice:
// If any value in a list is computed, this whole thing
// is computed and we can't read any part of it.
for i := 0; i < cv.Len(); i++ {
if v := cv.Index(i).Interface(); v == unknownValue() {
return v, false
}
}
default:
// If the value is just the unknown value, then we don't
// know anything beyond here.
if current == unknownValue() {
return current, false
}
}
return current, true
}
@ -364,3 +344,16 @@ func (c *ResourceConfig) interpolateForce() {
c.Raw = c.raw.RawMap()
c.Config = c.raw.Config()
}
// unknownCheckWalker
type unknownCheckWalker struct {
Unknown bool
}
func (w *unknownCheckWalker) Primitive(v reflect.Value) error {
if v.Interface() == unknownValue() {
w.Unknown = true
}
return nil
}

View File

@ -1,189 +0,0 @@
package terraform
import (
"reflect"
"testing"
)
func TestResourceConfig_CheckSet(t *testing.T) {
cases := []struct {
Raw map[string]interface{}
Computed []string
Input []string
Errs bool
}{
{
map[string]interface{}{
"foo": "bar",
},
nil,
[]string{"foo"},
false,
},
{
map[string]interface{}{
"foo": "bar",
},
nil,
[]string{"foo", "bar"},
true,
},
{
map[string]interface{}{
"foo": "bar",
},
[]string{"bar"},
[]string{"foo", "bar"},
false,
},
}
for i, tc := range cases {
rc := &ResourceConfig{
ComputedKeys: tc.Computed,
Raw: tc.Raw,
}
errs := rc.CheckSet(tc.Input)
if tc.Errs != (len(errs) > 0) {
t.Fatalf("bad: %d", i)
}
}
}
func TestResourceConfig_Get(t *testing.T) {
cases := []struct {
Raw map[string]interface{}
Computed []string
Input string
Output interface{}
OutputOk bool
}{
{
map[string]interface{}{
"foo": "bar",
},
nil,
"foo",
"bar",
true,
},
{
map[string]interface{}{},
nil,
"foo",
nil,
false,
},
{
map[string]interface{}{
"foo": map[interface{}]interface{}{
"bar": "baz",
},
},
nil,
"foo.bar",
"baz",
true,
},
{
map[string]interface{}{
"foo": []string{
"one",
"two",
},
},
nil,
"foo.1",
"two",
true,
},
}
for i, tc := range cases {
rc := &ResourceConfig{
ComputedKeys: tc.Computed,
Raw: tc.Raw,
}
actual, ok := rc.Get(tc.Input)
if tc.OutputOk != ok {
t.Fatalf("bad ok: %d", i)
}
if !reflect.DeepEqual(tc.Output, actual) {
t.Fatalf("bad %d: %#v", i, actual)
}
}
}
func TestResourceConfig_IsSet(t *testing.T) {
cases := []struct {
Raw map[string]interface{}
Computed []string
Input string
Output bool
}{
{
map[string]interface{}{
"foo": "bar",
},
nil,
"foo",
true,
},
{
map[string]interface{}{},
nil,
"foo",
false,
},
{
map[string]interface{}{},
[]string{"foo"},
"foo",
true,
},
{
map[string]interface{}{
"foo": map[interface{}]interface{}{
"bar": "baz",
},
},
nil,
"foo.bar",
true,
},
}
for i, tc := range cases {
rc := &ResourceConfig{
ComputedKeys: tc.Computed,
Raw: tc.Raw,
}
actual := rc.IsSet(tc.Input)
if actual != tc.Output {
t.Fatalf("fail case: %d", i)
}
}
}
func TestResourceConfig_IsSet_nil(t *testing.T) {
var rc *ResourceConfig
if rc.IsSet("foo") {
t.Fatal("bad")
}
}
func TestResourceProviderFactoryFixed(t *testing.T) {
p := new(MockResourceProvider)
var f ResourceProviderFactory = ResourceProviderFactoryFixed(p)
actual, err := f()
if err != nil {
t.Fatalf("err: %s", err)
}
if actual != p {
t.Fatal("should be identical")
}
}

View File

@ -8,6 +8,7 @@ import (
"github.com/hashicorp/hil"
"github.com/hashicorp/hil/ast"
"github.com/hashicorp/terraform/config"
"github.com/mitchellh/reflectwalk"
)
func TestInstanceInfo(t *testing.T) {
@ -58,6 +59,14 @@ func TestResourceConfigGet(t *testing.T) {
Value: nil,
},
{
Config: map[string]interface{}{
"foo": "bar",
},
Key: "foo",
Value: "bar",
},
{
Config: map[string]interface{}{
"foo": "${var.foo}",
@ -217,8 +226,6 @@ func TestResourceConfigGet(t *testing.T) {
// Test getting a key
t.Run(fmt.Sprintf("get-%d", i), func(t *testing.T) {
t.Logf("Config: %#v", rc)
v, _ := rc.Get(tc.Key)
if !reflect.DeepEqual(v, tc.Value) {
t.Fatalf("%d bad: %#v", i, v)
@ -297,6 +304,7 @@ func TestResourceConfigIsComputed(t *testing.T) {
Result: false,
},
/*
{
Name: "set count with computed elements",
Config: map[string]interface{}{
@ -311,6 +319,7 @@ func TestResourceConfigIsComputed(t *testing.T) {
Key: "foo.#",
Result: true,
},
*/
{
Name: "set count with computed elements",
@ -385,6 +394,100 @@ func TestResourceConfigIsComputed(t *testing.T) {
}
}
func TestResourceConfigCheckSet(t *testing.T) {
cases := []struct {
Name string
Config map[string]interface{}
Vars map[string]interface{}
Input []string
Errs bool
}{
{
Name: "computed basic",
Config: map[string]interface{}{
"foo": "${var.foo}",
},
Vars: map[string]interface{}{
"foo": unknownValue(),
},
Input: []string{"foo"},
Errs: false,
},
{
Name: "basic",
Config: map[string]interface{}{
"foo": "bar",
},
Vars: nil,
Input: []string{"foo"},
Errs: false,
},
{
Name: "basic with not set",
Config: map[string]interface{}{
"foo": "bar",
},
Vars: nil,
Input: []string{"foo", "bar"},
Errs: true,
},
{
Name: "basic with one computed",
Config: map[string]interface{}{
"foo": "bar",
"bar": "${var.foo}",
},
Vars: map[string]interface{}{
"foo": unknownValue(),
},
Input: []string{"foo", "bar"},
Errs: false,
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
var rawC *config.RawConfig
if tc.Config != nil {
var err error
rawC, err = config.NewRawConfig(tc.Config)
if err != nil {
t.Fatalf("err: %s", err)
}
}
if tc.Vars != nil {
vs := make(map[string]ast.Variable)
for k, v := range tc.Vars {
hilVar, err := hil.InterfaceToVariable(v)
if err != nil {
t.Fatalf("%#v to var: %s", v, err)
}
vs["var."+k] = hilVar
}
if err := rawC.Interpolate(vs); err != nil {
t.Fatalf("err: %s", err)
}
}
rc := NewResourceConfig(rawC)
rc.interpolateForce()
t.Logf("Config: %#v", rc)
errs := rc.CheckSet(tc.Input)
if tc.Errs != (len(errs) > 0) {
t.Fatalf("bad: %#v", errs)
}
})
}
}
func TestResourceConfigDeepCopy_nil(t *testing.T) {
var nilRc *ResourceConfig
actual := nilRc.DeepCopy()
@ -428,6 +531,54 @@ func TestResourceConfigEqual_computedKeyOrder(t *testing.T) {
}
}
func TestUnknownCheckWalker(t *testing.T) {
cases := []struct {
Name string
Input interface{}
Result bool
}{
{
"primitive",
42,
false,
},
{
"primitive computed",
unknownValue(),
true,
},
{
"list",
[]interface{}{"foo", unknownValue()},
true,
},
{
"nested list",
[]interface{}{
"foo",
[]interface{}{unknownValue()},
},
true,
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
var w unknownCheckWalker
if err := reflectwalk.Walk(tc.Input, &w); err != nil {
t.Fatalf("err: %s", err)
}
if w.Unknown != tc.Result {
t.Fatalf("bad: %v", w.Unknown)
}
})
}
}
func testResourceConfig(
t *testing.T, c map[string]interface{}) *ResourceConfig {
raw, err := config.NewRawConfig(c)