Merge pull request #9634 from hashicorp/jbardin/GH-9549

Fix for missing computed map entries disappearing
This commit is contained in:
Mitchell Hashimoto 2016-11-09 14:33:24 -08:00 committed by GitHub
commit b93331ba20
26 changed files with 765 additions and 374 deletions

View File

@ -149,6 +149,10 @@ func (v *ModuleVariable) FullKey() string {
return v.key
}
func (v *ModuleVariable) GoString() string {
return fmt.Sprintf("*%#v", *v)
}
func NewPathVariable(key string) (*PathVariable, error) {
var fieldType PathValueType
parts := strings.SplitN(key, ".", 2)

View File

@ -240,6 +240,22 @@ func TestDetectVariables(t *testing.T) {
},
},
},
{
`foo ${module.foo.output["key"]}`,
[]InterpolatedVariable{
&ModuleVariable{
Name: "foo",
Field: "output",
key: "module.foo.output",
},
&ModuleVariable{
Name: "foo",
Field: "output",
key: "module.foo.output",
},
},
},
}
for _, tc := range cases {

View File

@ -176,8 +176,7 @@ func (w *interpolationWalker) Primitive(v reflect.Value) error {
}
if remove {
w.removeCurrent()
return nil
w.unknownKeys = append(w.unknownKeys, strings.Join(w.key, "."))
}
resultVal := reflect.ValueOf(replaceVal)
@ -209,27 +208,6 @@ func (w *interpolationWalker) Primitive(v reflect.Value) error {
return nil
}
func (w *interpolationWalker) removeCurrent() {
// Append the key to the unknown keys
w.unknownKeys = append(w.unknownKeys, strings.Join(w.key, "."))
for i := 1; i <= len(w.cs); i++ {
c := w.cs[len(w.cs)-i]
switch c.Kind() {
case reflect.Map:
// Zero value so that we delete the map key
var val reflect.Value
// Get the key and delete it
k := w.csData.(reflect.Value)
c.SetMapIndex(k, val)
return
}
}
panic("No container found for removeCurrent")
}
func (w *interpolationWalker) replaceCurrent(v reflect.Value) {
c := w.cs[len(w.cs)-2]
switch c.Kind() {

View File

@ -169,8 +169,14 @@ func TestInterpolationWalker_replace(t *testing.T) {
"bing",
},
},
Output: map[string]interface{}{},
Value: []interface{}{UnknownVariableValue, "baz"},
Output: map[string]interface{}{
"foo": []interface{}{
UnknownVariableValue,
"baz",
"bing",
},
},
Value: []interface{}{UnknownVariableValue, "baz"},
},
}

View File

@ -127,27 +127,6 @@ func (r *RawConfig) Interpolate(vs map[string]ast.Variable) error {
config := langEvalConfig(vs)
return r.interpolate(func(root ast.Node) (interface{}, error) {
// We detect the variables again and check if the value of any
// of the variables is the computed value. If it is, then we
// treat this entire value as computed.
//
// We have to do this here before the `lang.Eval` because
// if any of the variables it depends on are computed, then
// the interpolation can fail at runtime for other reasons. Example:
// `${count.index+1}`: in a world where `count.index` is computed,
// this would fail a type check since the computed placeholder is
// a string, but realistically the whole value is just computed.
vars, err := DetectVariables(root)
if err != nil {
return "", err
}
for _, v := range vars {
varVal, ok := vs[v.FullKey()]
if ok && varVal.Value == UnknownVariableValue {
return UnknownVariableValue, nil
}
}
// None of the variables we need are computed, meaning we should
// be able to properly evaluate.
result, err := hil.Eval(root, config)

View File

@ -191,7 +191,7 @@ func TestRawConfig_merge(t *testing.T) {
},
"var.baz": ast.Variable{
Value: UnknownVariableValue,
Type: ast.TypeString,
Type: ast.TypeUnknown,
},
}
if err := rc2.Interpolate(vars); err != nil {
@ -216,6 +216,7 @@ func TestRawConfig_merge(t *testing.T) {
expected := map[string]interface{}{
"foo": "foovalue",
"bar": "barvalue",
"baz": UnknownVariableValue,
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
@ -250,7 +251,7 @@ func TestRawConfig_unknown(t *testing.T) {
vars := map[string]ast.Variable{
"var.bar": ast.Variable{
Value: UnknownVariableValue,
Type: ast.TypeString,
Type: ast.TypeUnknown,
},
}
if err := rc.Interpolate(vars); err != nil {
@ -258,7 +259,7 @@ func TestRawConfig_unknown(t *testing.T) {
}
actual := rc.Config()
expected := map[string]interface{}{}
expected := map[string]interface{}{"foo": UnknownVariableValue}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
@ -283,7 +284,7 @@ func TestRawConfig_unknownPartial(t *testing.T) {
vars := map[string]ast.Variable{
"var.bar": ast.Variable{
Value: UnknownVariableValue,
Type: ast.TypeString,
Type: ast.TypeUnknown,
},
}
if err := rc.Interpolate(vars); err != nil {
@ -291,7 +292,42 @@ func TestRawConfig_unknownPartial(t *testing.T) {
}
actual := rc.Config()
expected := map[string]interface{}{}
expected := map[string]interface{}{"foo": UnknownVariableValue}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
expectedKeys := []string{"foo"}
if !reflect.DeepEqual(rc.UnknownKeys(), expectedKeys) {
t.Fatalf("bad: %#v", rc.UnknownKeys())
}
}
func TestRawConfig_unknownPartialList(t *testing.T) {
raw := map[string]interface{}{
"foo": []interface{}{
"${var.bar}/32",
},
}
rc, err := NewRawConfig(raw)
if err != nil {
t.Fatalf("err: %s", err)
}
vars := map[string]ast.Variable{
"var.bar": ast.Variable{
Value: UnknownVariableValue,
Type: ast.TypeUnknown,
},
}
if err := rc.Interpolate(vars); err != nil {
t.Fatalf("err: %s", err)
}
actual := rc.Config()
expected := map[string]interface{}{"foo": []interface{}{UnknownVariableValue}}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)

View File

@ -3,6 +3,7 @@ package diff
import (
"strings"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/flatmap"
"github.com/hashicorp/terraform/terraform"
)
@ -94,7 +95,7 @@ func (b *ResourceBuilder) Diff(
// If this key is in the cleaned config, then use that value
// because it'll have its variables properly interpolated
if cleanV, ok := flatConfig[k]; ok {
if cleanV, ok := flatConfig[k]; ok && cleanV != config.UnknownVariableValue {
v = cleanV
originalV = v

View File

@ -344,7 +344,7 @@ func TestConfigFieldReader_ComputedSet(t *testing.T) {
}, map[string]ast.Variable{
"var.foo": ast.Variable{
Value: config.UnknownVariableValue,
Type: ast.TypeString,
Type: ast.TypeUnknown,
},
}),
false,
@ -362,7 +362,7 @@ func TestConfigFieldReader_ComputedSet(t *testing.T) {
}, map[string]ast.Variable{
"var.foo": ast.Variable{
Value: config.UnknownVariableValue,
Type: ast.TypeString,
Type: ast.TypeUnknown,
},
}),
false,

View File

@ -131,7 +131,8 @@ func interfaceToVariableSwallowError(input interface{}) ast.Variable {
}
func TestSchemaMap_Diff(t *testing.T) {
cases := map[string]struct {
cases := []struct {
Name string
Schema map[string]*Schema
State *terraform.InstanceState
Config map[string]interface{}
@ -139,7 +140,7 @@ func TestSchemaMap_Diff(t *testing.T) {
Diff *terraform.InstanceDiff
Err bool
}{
"#0": {
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
@ -168,7 +169,7 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#1": {
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
@ -195,7 +196,7 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#2": {
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
@ -216,7 +217,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#3 Computed, but set in config": {
{
Name: "Computed, but set in config",
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
@ -247,7 +249,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#4 Default": {
{
Name: "Default",
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
@ -272,7 +275,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#5 DefaultFunc, value": {
{
Name: "DefaultFunc, value",
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
@ -299,7 +303,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#6 DefaultFunc, configuration set": {
{
Name: "DefaultFunc, configuration set",
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
@ -328,7 +333,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"String with StateFunc": {
{
Name: "String with StateFunc",
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
@ -359,7 +365,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"StateFunc not called with nil value": {
{
Name: "StateFunc not called with nil value",
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
@ -389,7 +396,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#8 Variable (just checking)": {
{
Name: "Variable (just checking)",
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
@ -419,7 +427,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#9 Variable computed": {
{
Name: "Variable computed",
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
@ -450,7 +459,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#10 Int decode": {
{
Name: "Int decode",
Schema: map[string]*Schema{
"port": &Schema{
Type: TypeInt,
@ -479,7 +489,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#11 bool decode": {
{
Name: "bool decode",
Schema: map[string]*Schema{
"port": &Schema{
Type: TypeBool,
@ -508,7 +519,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#12 Bool": {
{
Name: "Bool",
Schema: map[string]*Schema{
"delete": &Schema{
Type: TypeBool,
@ -530,7 +542,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#13 List decode": {
{
Name: "List decode",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeList,
@ -569,7 +582,7 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#14": {
{
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeList,
@ -612,7 +625,7 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#15": {
{
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeList,
@ -645,7 +658,7 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#16": {
{
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeList,
@ -672,7 +685,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#17": {
{
Name: "",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeList,
@ -709,7 +723,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#18": {
{
Name: "",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeList,
@ -753,7 +768,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#19": {
{
Name: "",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeList,
@ -779,7 +795,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#20 Set": {
{
Name: "Set",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
@ -821,7 +838,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#21 Set": {
{
Name: "Set",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
@ -847,7 +865,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#22 Set": {
{
Name: "Set",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
@ -876,7 +895,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#23 Set": {
{
Name: "Set",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
@ -922,7 +942,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#24 Set": {
{
Name: "Set",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
@ -958,7 +979,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#25 Set": {
{
Name: "Set",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
@ -1006,7 +1028,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#26 Set": {
{
Name: "Set",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
@ -1050,7 +1073,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#27 Set": {
{
Name: "Set",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
@ -1078,7 +1102,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#28 Set": {
{
Name: "Set",
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeSet,
@ -1130,7 +1155,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#29 List of structure decode": {
{
Name: "List of structure decode",
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeList,
@ -1172,7 +1198,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#30 ComputedWhen": {
{
Name: "ComputedWhen",
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
@ -1202,7 +1229,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#31": {
{
Name: "",
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
@ -1280,7 +1308,8 @@ func TestSchemaMap_Diff(t *testing.T) {
},
*/
"#32 Maps": {
{
Name: "Maps",
Schema: map[string]*Schema{
"config_vars": &Schema{
Type: TypeMap,
@ -1314,7 +1343,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#33 Maps": {
{
Name: "Maps",
Schema: map[string]*Schema{
"config_vars": &Schema{
Type: TypeMap,
@ -1351,7 +1381,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#34 Maps": {
{
Name: "Maps",
Schema: map[string]*Schema{
"vars": &Schema{
Type: TypeMap,
@ -1391,7 +1422,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#35 Maps": {
{
Name: "Maps",
Schema: map[string]*Schema{
"vars": &Schema{
Type: TypeMap,
@ -1412,7 +1444,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#36 Maps": {
{
Name: "Maps",
Schema: map[string]*Schema{
"config_vars": &Schema{
Type: TypeList,
@ -1451,7 +1484,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#37 Maps": {
{
Name: "Maps",
Schema: map[string]*Schema{
"config_vars": &Schema{
Type: TypeList,
@ -1493,7 +1527,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#38 ForceNews": {
{
Name: "ForceNews",
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
@ -1538,7 +1573,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#39 Set": {
{
Name: "Set",
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
@ -1588,7 +1624,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#40 Set": {
{
Name: "Set",
Schema: map[string]*Schema{
"instances": &Schema{
Type: TypeSet,
@ -1626,7 +1663,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#41 Set": {
{
Name: "Set",
Schema: map[string]*Schema{
"route": &Schema{
Type: TypeSet,
@ -1687,7 +1725,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#42 Set": {
{
Name: "Set",
Schema: map[string]*Schema{
"route": &Schema{
Type: TypeSet,
@ -1752,7 +1791,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#43 - Computed maps": {
{
Name: "Computed maps",
Schema: map[string]*Schema{
"vars": &Schema{
Type: TypeMap,
@ -1776,7 +1816,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#44 - Computed maps": {
{
Name: "Computed maps",
Schema: map[string]*Schema{
"vars": &Schema{
Type: TypeMap,
@ -1812,7 +1853,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#45 - Empty": {
{
Name: " - Empty",
Schema: map[string]*Schema{},
State: &terraform.InstanceState{},
@ -1824,7 +1866,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#46 - Float": {
{
Name: "Float",
Schema: map[string]*Schema{
"some_threshold": &Schema{
Type: TypeFloat,
@ -1853,7 +1896,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#47 - https://github.com/hashicorp/terraform/issues/824": {
{
Name: "https://github.com/hashicorp/terraform/issues/824",
Schema: map[string]*Schema{
"block_device": &Schema{
Type: TypeSet,
@ -1906,7 +1950,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#48 - Zero value in state shouldn't result in diff": {
{
Name: "Zero value in state shouldn't result in diff",
Schema: map[string]*Schema{
"port": &Schema{
Type: TypeBool,
@ -1928,7 +1973,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#49 Set - Same as #48 but for sets": {
{
Name: "Same as prev, but for sets",
Schema: map[string]*Schema{
"route": &Schema{
Type: TypeSet,
@ -1970,7 +2016,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#50 - A set computed element shouldn't cause a diff": {
{
Name: "A set computed element shouldn't cause a diff",
Schema: map[string]*Schema{
"active": &Schema{
Type: TypeBool,
@ -1992,7 +2039,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#51 - An empty set should show up in the diff": {
{
Name: "An empty set should show up in the diff",
Schema: map[string]*Schema{
"instances": &Schema{
Type: TypeSet,
@ -2033,7 +2081,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#52 - Map with empty value": {
{
Name: "Map with empty value",
Schema: map[string]*Schema{
"vars": &Schema{
Type: TypeMap,
@ -2064,7 +2113,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#53 - Unset bool, not in state": {
{
Name: "Unset bool, not in state",
Schema: map[string]*Schema{
"force": &Schema{
Type: TypeBool,
@ -2082,7 +2132,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#54 - Unset set, not in state": {
{
Name: "Unset set, not in state",
Schema: map[string]*Schema{
"metadata_keys": &Schema{
Type: TypeSet,
@ -2102,7 +2153,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#55 - Unset list in state, should not show up computed": {
{
Name: "Unset list in state, should not show up computed",
Schema: map[string]*Schema{
"metadata_keys": &Schema{
Type: TypeList,
@ -2126,7 +2178,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#56 - Set element computed substring": {
{
Name: "Set element computed substring",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
@ -2161,7 +2214,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#57 Computed map without config that's known to be empty does not generate diff": {
{
Name: "Computed map without config that's known to be empty does not generate diff",
Schema: map[string]*Schema{
"tags": &Schema{
Type: TypeMap,
@ -2182,7 +2236,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#58 Set with hyphen keys": {
{
Name: "Set with hyphen keys",
Schema: map[string]*Schema{
"route": &Schema{
Type: TypeSet,
@ -2238,7 +2293,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#59: StateFunc in nested set (#1759)": {
{
Name: ": StateFunc in nested set (#1759)",
Schema: map[string]*Schema{
"service_account": &Schema{
Type: TypeList,
@ -2303,7 +2359,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"#60 - Removing set elements": {
{
Name: "Removing set elements",
Schema: map[string]*Schema{
"instances": &Schema{
Type: TypeSet,
@ -2355,7 +2412,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"Bools can be set with 0/1 in config, still get true/false": {
{
Name: "Bools can be set with 0/1 in config, still get true/false",
Schema: map[string]*Schema{
"one": &Schema{
Type: TypeBool,
@ -2405,7 +2463,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"tainted in state w/ no attr changes is still a replacement": {
{
Name: "tainted in state w/ no attr changes is still a replacement",
Schema: map[string]*Schema{},
State: &terraform.InstanceState{
@ -2425,7 +2484,8 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
"Set ForceNew only marks the changing element as ForceNew": {
{
Name: "Set ForceNew only marks the changing element as ForceNew",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
@ -2480,7 +2540,8 @@ func TestSchemaMap_Diff(t *testing.T) {
},
},
"removed optional items should trigger ForceNew": {
{
Name: "removed optional items should trigger ForceNew",
Schema: map[string]*Schema{
"description": &Schema{
Type: TypeString,
@ -2512,7 +2573,8 @@ func TestSchemaMap_Diff(t *testing.T) {
},
// GH-7715
"computed value for boolean field": {
{
Name: "computed value for boolean field",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeBool,
@ -2548,27 +2610,27 @@ func TestSchemaMap_Diff(t *testing.T) {
},
}
for tn, tc := range cases {
t.Run(tn, func(t *testing.T) {
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
c, err := config.NewRawConfig(tc.Config)
if err != nil {
t.Fatalf("#%q err: %s", tn, err)
t.Fatalf("err: %s", err)
}
if len(tc.ConfigVariables) > 0 {
if err := c.Interpolate(tc.ConfigVariables); err != nil {
t.Fatalf("#%q err: %s", tn, err)
t.Fatalf("err: %s", err)
}
}
d, err := schemaMap(tc.Schema).Diff(
tc.State, terraform.NewResourceConfig(c))
if err != nil != tc.Err {
t.Fatalf("#%q err: %s", tn, err)
t.Fatalf("err: %s", err)
}
if !reflect.DeepEqual(tc.Diff, d) {
t.Fatalf("#%q:\n\nexpected:\n%#v\n\ngot:\n%#v", tn, tc.Diff, d)
t.Fatalf("expected:\n%#v\n\ngot:\n%#v", tc.Diff, d)
}
})
}

View File

@ -87,7 +87,9 @@ func TestContext2Validate_computedVar(t *testing.T) {
t.Fatalf("bad: %#v", w)
}
if len(e) > 0 {
t.Fatalf("bad: %#v", e)
for _, err := range e {
t.Errorf("bad: %s", err)
}
}
}
@ -821,3 +823,29 @@ func TestContext2Validate_interpolateComputedModuleVarDef(t *testing.T) {
t.Fatal("err:", e)
}
}
// Computed values are lost when a map is output from a module
func TestContext2Validate_interpolateMap(t *testing.T) {
input := new(MockUIInput)
m := testModule(t, "issue-9549")
p := testProvider("null")
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"template": testProviderFuncFixed(p),
},
UIInput: input,
})
w, e := ctx.Validate()
if w != nil {
t.Log("warnings:", w)
}
if e != nil {
t.Fatal("err:", e)
}
}

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

@ -121,11 +121,15 @@ func (i *Interpolater) valueCountVar(
func unknownVariable() ast.Variable {
return ast.Variable{
Type: ast.TypeString,
Type: ast.TypeUnknown,
Value: config.UnknownVariableValue,
}
}
func unknownValue() string {
return hil.UnknownValue
}
func (i *Interpolater) valueModuleVar(
scope *InterpolationScope,
n string,
@ -215,10 +219,7 @@ func (i *Interpolater) valueResourceVar(
// If we're computing all dynamic fields, then module vars count
// and we mark it as computed.
if i.Operation == walkValidate {
result[n] = ast.Variable{
Value: config.UnknownVariableValue,
Type: ast.TypeString,
}
result[n] = unknownVariable()
return nil
}
@ -243,10 +244,7 @@ func (i *Interpolater) valueResourceVar(
// This applies only to graph nodes that interpolate during the
// config walk, e.g. providers.
if i.Operation == walkInput {
result[n] = ast.Variable{
Value: config.UnknownVariableValue,
Type: ast.TypeString,
}
result[n] = unknownVariable()
return nil
}

View File

@ -306,7 +306,7 @@ func TestInterpolater_resourceVariableMissingDuringInput(t *testing.T) {
testInterpolate(t, i, scope, "aws_instance.web.foo", ast.Variable{
Value: config.UnknownVariableValue,
Type: ast.TypeString,
Type: ast.TypeUnknown,
})
}
@ -360,7 +360,7 @@ func TestInterpolater_resourceVariableMulti(t *testing.T) {
testInterpolate(t, i, scope, "aws_instance.web.*.foo", ast.Variable{
Value: config.UnknownVariableValue,
Type: ast.TypeString,
Type: ast.TypeUnknown,
})
}
@ -565,7 +565,7 @@ func TestInterpolator_resourceMultiAttributesComputed(t *testing.T) {
testInterpolate(t, i, scope, "aws_route53_zone.yada.name_servers", ast.Variable{
Value: config.UnknownVariableValue,
Type: ast.TypeString,
Type: ast.TypeUnknown,
})
}

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,9 +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 {
_, ok := c.get(k, c.Config)
_, okRaw := c.get(k, c.Raw)
return !ok && okRaw
// 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)
if !ok {
return false
}
// 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
@ -225,10 +243,8 @@ func (c *ResourceConfig) IsSet(k string) bool {
return false
}
for _, ck := range c.ComputedKeys {
if ck == k {
return true
}
if c.IsComputed(k) {
return true
}
if _, ok := c.Get(k); ok {
@ -264,15 +280,26 @@ func (c *ResourceConfig) get(
if !v.IsValid() {
return nil, false
}
return v.Interface(), true
}
return nil, false
}
current = v.Interface()
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, true
}
}
current = cv.Len()
} else {
i, err := strconv.ParseInt(part, 0, 0)
@ -291,6 +318,7 @@ func (c *ResourceConfig) get(
if prevMap, ok := previous.(map[string]interface{}); ok {
return prevMap[actualKey], true
}
return nil, false
default:
panic(fmt.Sprintf("Unknown kind: %s", cv.Kind()))
@ -316,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

@ -5,8 +5,10 @@ import (
"reflect"
"testing"
"github.com/hashicorp/hil"
"github.com/hashicorp/hil/ast"
"github.com/hashicorp/terraform/config"
"github.com/mitchellh/reflectwalk"
)
func TestInstanceInfo(t *testing.T) {
@ -47,7 +49,7 @@ func TestInstanceInfo(t *testing.T) {
func TestResourceConfigGet(t *testing.T) {
cases := []struct {
Config map[string]interface{}
Vars map[string]string
Vars map[string]interface{}
Key string
Value interface{}
}{
@ -57,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}",
@ -69,9 +79,9 @@ func TestResourceConfigGet(t *testing.T) {
Config: map[string]interface{}{
"foo": "${var.foo}",
},
Vars: map[string]string{"foo": "bar"},
Vars: map[string]interface{}{"foo": unknownValue()},
Key: "foo",
Value: "bar",
Value: "${var.foo}",
},
{
@ -198,7 +208,12 @@ func TestResourceConfigGet(t *testing.T) {
if tc.Vars != nil {
vs := make(map[string]ast.Variable)
for k, v := range tc.Vars {
vs["var."+k] = ast.Variable{Value: v, Type: ast.TypeString}
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 {
@ -239,6 +254,240 @@ func TestResourceConfigGet(t *testing.T) {
}
}
func TestResourceConfigIsComputed(t *testing.T) {
cases := []struct {
Name string
Config map[string]interface{}
Vars map[string]interface{}
Key string
Result bool
}{
{
Name: "basic value",
Config: map[string]interface{}{
"foo": "${var.foo}",
},
Vars: map[string]interface{}{
"foo": unknownValue(),
},
Key: "foo",
Result: true,
},
{
Name: "set with a computed element",
Config: map[string]interface{}{
"foo": "${var.foo}",
},
Vars: map[string]interface{}{
"foo": []string{
"a",
unknownValue(),
},
},
Key: "foo",
Result: true,
},
{
Name: "set with no computed elements",
Config: map[string]interface{}{
"foo": "${var.foo}",
},
Vars: map[string]interface{}{
"foo": []string{
"a",
"b",
},
},
Key: "foo",
Result: false,
},
/*
{
Name: "set count with computed elements",
Config: map[string]interface{}{
"foo": "${var.foo}",
},
Vars: map[string]interface{}{
"foo": []string{
"a",
unknownValue(),
},
},
Key: "foo.#",
Result: true,
},
*/
{
Name: "set count with computed elements",
Config: map[string]interface{}{
"foo": []interface{}{"${var.foo}"},
},
Vars: map[string]interface{}{
"foo": []string{
"a",
unknownValue(),
},
},
Key: "foo.#",
Result: true,
},
{
Name: "nested set with computed elements",
Config: map[string]interface{}{
"route": []map[string]interface{}{
map[string]interface{}{
"index": "1",
"gateway": []interface{}{"${var.foo}"},
},
},
},
Vars: map[string]interface{}{
"foo": unknownValue(),
},
Key: "route.0.gateway",
Result: true,
},
}
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)
actual := rc.IsComputed(tc.Key)
if actual != tc.Result {
t.Fatalf("bad: %#v", actual)
}
})
}
}
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()
@ -282,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)

View File

@ -0,0 +1,7 @@
module "mod" {
source = "./mod"
}
resource "template_file" "root_template" {
template = "ext: ${module.mod.base_config["base_template"]}"
}

View File

@ -0,0 +1,12 @@
resource "template_file" "example" {
template = "template text"
}
output "base_config" {
value = {
base_template = "${template_file.example.rendered}"
# without this we fail with no entries
extra = "value"
}
}

View File

@ -54,6 +54,13 @@ const (
TypeFloat
TypeList
TypeMap
// This is a special type used by Terraform to mark "unknown" values.
// It is impossible for this type to be introduced into your HIL programs
// unless you explicitly set a variable to this value. In that case,
// any operation including the variable will return "TypeUnknown" as the
// type.
TypeUnknown
)
func (t Type) Printable() string {
@ -72,6 +79,8 @@ func (t Type) Printable() string {
return "type list"
case TypeMap:
return "type map"
case TypeUnknown:
return "type unknown"
default:
return "unknown type"
}

30
vendor/github.com/hashicorp/hil/ast/unknown.go generated vendored Normal file
View File

@ -0,0 +1,30 @@
package ast
// IsUnknown reports whether a variable is unknown or contains any value
// that is unknown. This will recurse into lists and maps and so on.
func IsUnknown(v Variable) bool {
// If it is unknown itself, return true
if v.Type == TypeUnknown {
return true
}
// If it is a container type, check the values
switch v.Type {
case TypeList:
for _, el := range v.Value.([]Variable) {
if IsUnknown(el) {
return true
}
}
case TypeMap:
for _, el := range v.Value.(map[string]Variable) {
if IsUnknown(el) {
return true
}
}
default:
}
// Not a container type or survive the above checks
return false
}

View File

@ -5,6 +5,11 @@ import "fmt"
func VariableListElementTypesAreHomogenous(variableName string, list []Variable) (Type, error) {
listTypes := make(map[Type]struct{})
for _, v := range list {
// Allow unknown
if v.Type == TypeUnknown {
continue
}
if _, ok := listTypes[v.Type]; ok {
continue
}
@ -25,9 +30,15 @@ func VariableListElementTypesAreHomogenous(variableName string, list []Variable)
func VariableMapValueTypesAreHomogenous(variableName string, vmap map[string]Variable) (Type, error) {
valueTypes := make(map[Type]struct{})
for _, v := range vmap {
// Allow unknown
if v.Type == TypeUnknown {
continue
}
if _, ok := valueTypes[v.Type]; ok {
continue
}
valueTypes[v.Type] = struct{}{}
}

View File

@ -44,6 +44,12 @@ func (v *TypeCheck) Visit(root ast.Node) error {
defer v.lock.Unlock()
defer v.reset()
root.Accept(v.visit)
// If the resulting type is unknown, then just let the whole thing go.
if v.err == errExitUnknown {
v.err = nil
}
return v.err
}
@ -89,6 +95,10 @@ func (v *TypeCheck) visit(raw ast.Node) ast.Node {
pos.Column, pos.Line, err)
}
if v.StackPeek() == ast.TypeUnknown {
v.err = errExitUnknown
}
return result
}
@ -242,15 +252,14 @@ func (tc *typeCheckOutput) TypeCheck(v *TypeCheck) (ast.Node, error) {
}
// If there is only one argument and it is a list, we evaluate to a list
if len(types) == 1 && types[0] == ast.TypeList {
v.StackPush(ast.TypeList)
return n, nil
}
// If there is only one argument and it is a map, we evaluate to a map
if len(types) == 1 && types[0] == ast.TypeMap {
v.StackPush(ast.TypeMap)
return n, nil
if len(types) == 1 {
switch t := types[0]; t {
case ast.TypeList:
fallthrough
case ast.TypeMap:
v.StackPush(t)
return n, nil
}
}
// Otherwise, all concat args must be strings, so validate that
@ -294,6 +303,13 @@ func (tc *typeCheckVariableAccess) TypeCheck(v *TypeCheck) (ast.Node, error) {
"unknown variable accessed: %s", tc.n.Name)
}
// Check if the variable contains any unknown types. If so, then
// mark it as unknown.
if ast.IsUnknown(variable) {
v.StackPush(ast.TypeUnknown)
return tc.n, nil
}
// Add the type to the stack
v.StackPush(variable.Type)
@ -399,3 +415,11 @@ func (v *TypeCheck) StackPop() ast.Type {
x, v.Stack = v.Stack[len(v.Stack)-1], v.Stack[:len(v.Stack)-1]
return x
}
func (v *TypeCheck) StackPeek() ast.Type {
if len(v.Stack) == 0 {
return ast.TypeInvalid
}
return v.Stack[len(v.Stack)-1]
}

View File

@ -8,6 +8,11 @@ import (
"github.com/mitchellh/mapstructure"
)
// UnknownValue is a sentinel value that can be used to denote
// that a value of a variable (or map element, list element, etc.)
// is unknown. This will always have the type ast.TypeUnknown.
const UnknownValue = "74D93920-ED26-11E3-AC10-0800200C9A66"
var hilMapstructureDecodeHookSlice []interface{}
var hilMapstructureDecodeHookStringSlice []string
var hilMapstructureDecodeHookMap map[string]interface{}
@ -48,6 +53,12 @@ func InterfaceToVariable(input interface{}) (ast.Variable, error) {
var stringVal string
if err := hilMapstructureWeakDecode(input, &stringVal); err == nil {
// Special case the unknown value to turn into "unknown"
if stringVal == UnknownValue {
return ast.Variable{Type: ast.TypeUnknown}, nil
}
// Otherwise return the string value
return ast.Variable{
Type: ast.TypeString,
Value: stringVal,

View File

@ -2,6 +2,7 @@ package hil
import (
"bytes"
"errors"
"fmt"
"sync"
@ -42,6 +43,10 @@ type EvaluationResult struct {
// The error is described out of band in the accompanying error return value.
var InvalidResult = EvaluationResult{Type: TypeInvalid, Value: nil}
// errExitUnknown is an internal error that when returned means the result
// is an unknown value. We use this for early exit.
var errExitUnknown = errors.New("unknown value")
func Eval(root ast.Node, config *EvalConfig) (EvaluationResult, error) {
output, outputType, err := internalEval(root, config)
if err != nil {
@ -72,6 +77,11 @@ func Eval(root ast.Node, config *EvalConfig) (EvaluationResult, error) {
Type: TypeString,
Value: output,
}, nil
case ast.TypeUnknown:
return EvaluationResult{
Type: TypeUnknown,
Value: UnknownValue,
}, nil
default:
return InvalidResult, fmt.Errorf("unknown type %s as interpolation output", outputType)
}
@ -154,6 +164,12 @@ func (v *evalVisitor) Visit(root ast.Node) (interface{}, ast.Type, error) {
result = new(ast.LiteralNode)
}
resultErr := v.err
if resultErr == errExitUnknown {
// This means the return value is unknown and we used the error
// as an early exit mechanism. Reset since the value on the stack
// should be the unknown value.
resultErr = nil
}
// Clear everything else so we aren't just dangling
v.Stack.Reset()
@ -188,6 +204,13 @@ func (v *evalVisitor) visit(raw ast.Node) ast.Node {
Value: out,
Typex: outType,
})
if outType == ast.TypeUnknown {
// Halt immediately
v.err = errExitUnknown
return raw
}
return raw
}
@ -330,11 +353,15 @@ func (v *evalOutput) Eval(s ast.Scope, stack *ast.Stack) (interface{}, ast.Type,
}
// Special case the single list and map
if len(nodes) == 1 && nodes[0].Typex == ast.TypeList {
return nodes[0].Value, ast.TypeList, nil
}
if len(nodes) == 1 && nodes[0].Typex == ast.TypeMap {
return nodes[0].Value, ast.TypeMap, nil
if len(nodes) == 1 {
switch t := nodes[0].Typex; t {
case ast.TypeList:
fallthrough
case ast.TypeMap:
fallthrough
case ast.TypeUnknown:
return nodes[0].Value, t, nil
}
}
// Otherwise concatenate the strings
@ -362,5 +389,11 @@ func (v *evalVariableAccess) Eval(scope ast.Scope, _ *ast.Stack) (interface{}, a
"unknown variable accessed: %s", v.Name)
}
// Check if the variable contains any unknown types. If so, then
// mark it as unknown and return that type.
if ast.IsUnknown(variable) {
return nil, ast.TypeUnknown, nil
}
return variable.Value, variable.Type, nil
}

View File

@ -11,4 +11,5 @@ const (
TypeString EvalType = 1 << iota
TypeList
TypeMap
TypeUnknown
)

12
vendor/vendor.json vendored
View File

@ -1386,16 +1386,16 @@
"revisionTime": "2016-11-09T00:00:27Z"
},
{
"checksumSHA1": "RYz/9y1RMZfg+oMgEyJIWiSl1dU=",
"checksumSHA1": "mZhRldYjh9MAXzdi3zihMX0A/JU=",
"path": "github.com/hashicorp/hil",
"revision": "3e00ff29065d64c0f8e9ef7efed82686bbda81ca",
"revisionTime": "2016-10-14T17:08:44Z"
"revision": "ce4ab742a9dd2bb6e55050337333b2c56666e5a0",
"revisionTime": "2016-10-27T15:25:34Z"
},
{
"checksumSHA1": "WYIQ+nJPa191qpQIUsauF4wXYSw=",
"checksumSHA1": "FFroNUb6Nn6xUQJMsVDTb4Cqzo4=",
"path": "github.com/hashicorp/hil/ast",
"revision": "3e00ff29065d64c0f8e9ef7efed82686bbda81ca",
"revisionTime": "2016-10-14T17:08:44Z"
"revision": "ce4ab742a9dd2bb6e55050337333b2c56666e5a0",
"revisionTime": "2016-10-27T15:25:34Z"
},
{
"path": "github.com/hashicorp/logutils",