core: support native list variables in config

This commit adds support for native list variables and outputs, building
up on the previous change to state. Interpolation functions now return
native lists in preference to StringList.

List variables are defined like this:

variable "test" {
    # This can also be inferred
    type = "list"
    default = ["Hello", "World"]
}

output "test_out" {
    value = "${var.a_list}"
}
This results in the following state:

```
...
            "outputs": {
                "test_out": [
                    "hello",
                    "world"
                ]
            },
...
```

And the result of terraform output is as follows:

```
$ terraform output
test_out = [
  hello
  world
]
```

Using the output name, an xargs-friendly representation is output:

```
$ terraform output test_out
hello
world
```

The output command also supports indexing into the list (with
appropriate range checking and no wrapping):

```
$ terraform output test_out 1
world
```

Along with maps, list outputs from one module may be passed as variables
into another, removing the need for the `join(",", var.list_as_string)`
and `split(",", var.list_as_string)` which was previously necessary in
Terraform configuration.

This commit also updates the tests and implementations of built-in
interpolation functions to take and return native lists where
appropriate.

A backwards compatibility note: previously the concat interpolation
function was capable of concatenating either strings or lists. The
strings use case was deprectated a long time ago but still remained.
Because we cannot return `ast.TypeAny` from an interpolation function,
this use case is no longer supported for strings - `concat` is only
capable of concatenating lists. This should not be a huge issue - the
type checker picks up incorrect parameters, and the native HIL string
concatenation - or the `join` function - can be used to replicate the
missing behaviour.
This commit is contained in:
James Nugent 2016-04-21 17:03:24 -07:00
parent a49b17147a
commit f49583d25a
16 changed files with 425 additions and 331 deletions

View File

@ -161,6 +161,7 @@ type VariableType byte
const ( const (
VariableTypeUnknown VariableType = iota VariableTypeUnknown VariableType = iota
VariableTypeString VariableTypeString
VariableTypeList
VariableTypeMap VariableTypeMap
) )
@ -170,6 +171,8 @@ func (v VariableType) Printable() string {
return "string" return "string"
case VariableTypeMap: case VariableTypeMap:
return "map" return "map"
case VariableTypeList:
return "list"
default: default:
return "unknown" return "unknown"
} }
@ -351,16 +354,30 @@ func (c *Config) Validate() error {
m.Id())) m.Id()))
} }
// Check that the configuration can all be strings // Check that the configuration can all be strings, lists or maps
raw := make(map[string]interface{}) raw := make(map[string]interface{})
for k, v := range m.RawConfig.Raw { for k, v := range m.RawConfig.Raw {
var strVal string var strVal string
if err := mapstructure.WeakDecode(v, &strVal); err != nil { if err := mapstructure.WeakDecode(v, &strVal); err == nil {
errs = append(errs, fmt.Errorf(
"%s: variable %s must be a string value",
m.Id(), k))
}
raw[k] = strVal raw[k] = strVal
continue
}
var mapVal map[string]interface{}
if err := mapstructure.WeakDecode(v, &mapVal); err == nil {
raw[k] = mapVal
continue
}
var sliceVal []interface{}
if err := mapstructure.WeakDecode(v, &sliceVal); err == nil {
raw[k] = sliceVal
continue
}
errs = append(errs, fmt.Errorf(
"%s: variable %s must be a string, list or map value",
m.Id(), k))
} }
// Check for invalid count variables // Check for invalid count variables
@ -721,7 +738,8 @@ func (c *Config) validateVarContextFn(
if rv.Multi && rv.Index == -1 { if rv.Multi && rv.Index == -1 {
*errs = append(*errs, fmt.Errorf( *errs = append(*errs, fmt.Errorf(
"%s: multi-variable must be in a slice", source)) "%s: use of the splat ('*') operator must be wrapped in a list declaration",
source))
} }
} }
} }
@ -829,6 +847,7 @@ func (v *Variable) Merge(v2 *Variable) *Variable {
var typeStringMap = map[string]VariableType{ var typeStringMap = map[string]VariableType{
"string": VariableTypeString, "string": VariableTypeString,
"map": VariableTypeMap, "map": VariableTypeMap,
"list": VariableTypeList,
} }
// Type returns the type of variable this is. // Type returns the type of variable this is.
@ -888,9 +907,9 @@ func (v *Variable) inferTypeFromDefault() VariableType {
return VariableTypeString return VariableTypeString
} }
var strVal string var s string
if err := mapstructure.WeakDecode(v.Default, &strVal); err == nil { if err := mapstructure.WeakDecode(v.Default, &s); err == nil {
v.Default = strVal v.Default = s
return VariableTypeString return VariableTypeString
} }
@ -900,5 +919,11 @@ func (v *Variable) inferTypeFromDefault() VariableType {
return VariableTypeMap return VariableTypeMap
} }
var l []string
if err := mapstructure.WeakDecode(v.Default, &l); err == nil {
v.Default = l
return VariableTypeList
}
return VariableTypeUnknown return VariableTypeUnknown
} }

View File

@ -215,8 +215,15 @@ func TestConfigValidate_moduleVarInt(t *testing.T) {
func TestConfigValidate_moduleVarMap(t *testing.T) { func TestConfigValidate_moduleVarMap(t *testing.T) {
c := testConfig(t, "validate-module-var-map") c := testConfig(t, "validate-module-var-map")
if err := c.Validate(); err == nil { if err := c.Validate(); err != nil {
t.Fatal("should be invalid") t.Fatalf("should be valid: %s", err)
}
}
func TestConfigValidate_moduleVarList(t *testing.T) {
c := testConfig(t, "validate-module-var-list")
if err := c.Validate(); err != nil {
t.Fatalf("should be valid: %s", err)
} }
} }
@ -367,10 +374,10 @@ func TestConfigValidate_varDefault(t *testing.T) {
} }
} }
func TestConfigValidate_varDefaultBadType(t *testing.T) { func TestConfigValidate_varDefaultListType(t *testing.T) {
c := testConfig(t, "validate-var-default-bad-type") c := testConfig(t, "validate-var-default-list-type")
if err := c.Validate(); err == nil { if err := c.Validate(); err != nil {
t.Fatal("should not be valid") t.Fatal("should be valid: %s", err)
} }
} }

View File

@ -284,18 +284,35 @@ func DetectVariables(root ast.Node) ([]InterpolatedVariable, error) {
return n return n
} }
vn, ok := n.(*ast.VariableAccess) switch vn := n.(type) {
if !ok { case *ast.VariableAccess:
return n
}
v, err := NewInterpolatedVariable(vn.Name) v, err := NewInterpolatedVariable(vn.Name)
if err != nil { if err != nil {
resultErr = err resultErr = err
return n return n
} }
result = append(result, v) result = append(result, v)
case *ast.Index:
if va, ok := vn.Target.(*ast.VariableAccess); ok {
v, err := NewInterpolatedVariable(va.Name)
if err != nil {
resultErr = err
return n
}
result = append(result, v)
}
if va, ok := vn.Key.(*ast.VariableAccess); ok {
v, err := NewInterpolatedVariable(va.Name)
if err != nil {
resultErr = err
return n
}
result = append(result, v)
}
default:
return n
}
return n return n
} }

View File

@ -1,7 +1,6 @@
package config package config
import ( import (
"bytes"
"crypto/md5" "crypto/md5"
"crypto/sha1" "crypto/sha1"
"crypto/sha256" "crypto/sha256"
@ -24,6 +23,31 @@ import (
"github.com/mitchellh/go-homedir" "github.com/mitchellh/go-homedir"
) )
// stringSliceToVariableValue converts a string slice into the value
// required to be returned from interpolation functions which return
// TypeList.
func stringSliceToVariableValue(values []string) []ast.Variable {
output := make([]ast.Variable, len(values))
for index, value := range values {
output[index] = ast.Variable{
Type: ast.TypeString,
Value: value,
}
}
return output
}
func listVariableValueToStringSlice(values []ast.Variable) ([]string, error) {
output := make([]string, len(values))
for index, value := range values {
if value.Type != ast.TypeString {
return []string{}, fmt.Errorf("list has non-string element (%T)", value.Type.String())
}
output[index] = value.Value.(string)
}
return output, nil
}
// Funcs is the mapping of built-in functions for configuration. // Funcs is the mapping of built-in functions for configuration.
func Funcs() map[string]ast.Function { func Funcs() map[string]ast.Function {
return map[string]ast.Function{ return map[string]ast.Function{
@ -61,14 +85,23 @@ func Funcs() map[string]ast.Function {
// (e.g. as returned by "split") of any empty strings. // (e.g. as returned by "split") of any empty strings.
func interpolationFuncCompact() ast.Function { func interpolationFuncCompact() ast.Function {
return ast.Function{ return ast.Function{
ArgTypes: []ast.Type{ast.TypeString}, ArgTypes: []ast.Type{ast.TypeList},
ReturnType: ast.TypeString, ReturnType: ast.TypeList,
Variadic: false, Variadic: false,
Callback: func(args []interface{}) (interface{}, error) { Callback: func(args []interface{}) (interface{}, error) {
if !IsStringList(args[0].(string)) { inputList := args[0].([]ast.Variable)
return args[0].(string), nil
var outputList []string
for _, val := range inputList {
if strVal, ok := val.Value.(string); ok {
if strVal == "" {
continue
} }
return StringList(args[0].(string)).Compact().String(), nil
outputList = append(outputList, strVal)
}
}
return stringSliceToVariableValue(outputList), nil
}, },
} }
} }
@ -189,39 +222,32 @@ func interpolationFuncCoalesce() ast.Function {
// compat we do this. // compat we do this.
func interpolationFuncConcat() ast.Function { func interpolationFuncConcat() ast.Function {
return ast.Function{ return ast.Function{
ArgTypes: []ast.Type{ast.TypeString}, ArgTypes: []ast.Type{ast.TypeAny},
ReturnType: ast.TypeString, ReturnType: ast.TypeList,
Variadic: true, Variadic: true,
VariadicType: ast.TypeString, VariadicType: ast.TypeAny,
Callback: func(args []interface{}) (interface{}, error) { Callback: func(args []interface{}) (interface{}, error) {
var b bytes.Buffer var finalListElements []string
var finalList []string
var isDeprecated = true
for _, arg := range args { for _, arg := range args {
argument := arg.(string) // Append strings for backward compatibility
if argument, ok := arg.(string); ok {
if len(argument) == 0 { finalListElements = append(finalListElements, argument)
continue continue
} }
if IsStringList(argument) { // Otherwise variables
isDeprecated = false if argument, ok := arg.([]ast.Variable); ok {
finalList = append(finalList, StringList(argument).Slice()...) for _, element := range argument {
} else { finalListElements = append(finalListElements, element.Value.(string))
finalList = append(finalList, argument) }
continue
} }
// Deprecated concat behaviour return nil, fmt.Errorf("arguments to concat() must be a string or list")
b.WriteString(argument)
} }
if isDeprecated { return stringSliceToVariableValue(finalListElements), nil
return b.String(), nil
}
return NewStringList(finalList).String(), nil
}, },
} }
} }
@ -266,10 +292,10 @@ func interpolationFuncFormat() ast.Function {
// string formatting on lists. // string formatting on lists.
func interpolationFuncFormatList() ast.Function { func interpolationFuncFormatList() ast.Function {
return ast.Function{ return ast.Function{
ArgTypes: []ast.Type{ast.TypeString}, ArgTypes: []ast.Type{ast.TypeAny},
Variadic: true, Variadic: true,
VariadicType: ast.TypeAny, VariadicType: ast.TypeAny,
ReturnType: ast.TypeString, ReturnType: ast.TypeList,
Callback: func(args []interface{}) (interface{}, error) { Callback: func(args []interface{}) (interface{}, error) {
// Make a copy of the variadic part of args // Make a copy of the variadic part of args
// to avoid modifying the original. // to avoid modifying the original.
@ -280,15 +306,15 @@ func interpolationFuncFormatList() ast.Function {
// Confirm along the way that all lists have the same length (n). // Confirm along the way that all lists have the same length (n).
var n int var n int
for i := 1; i < len(args); i++ { for i := 1; i < len(args); i++ {
s, ok := args[i].(string) s, ok := args[i].([]ast.Variable)
if !ok { if !ok {
continue continue
} }
if !IsStringList(s) {
continue
}
parts := StringList(s).Slice() parts, err := listVariableValueToStringSlice(s)
if err != nil {
return nil, err
}
// otherwise the list is sent down to be indexed // otherwise the list is sent down to be indexed
varargs[i-1] = parts varargs[i-1] = parts
@ -325,7 +351,7 @@ func interpolationFuncFormatList() ast.Function {
} }
list[i] = fmt.Sprintf(format, fmtargs...) list[i] = fmt.Sprintf(format, fmtargs...)
} }
return NewStringList(list).String(), nil return stringSliceToVariableValue(list), nil
}, },
} }
} }
@ -334,13 +360,13 @@ func interpolationFuncFormatList() ast.Function {
// find the index of a specific element in a list // find the index of a specific element in a list
func interpolationFuncIndex() ast.Function { func interpolationFuncIndex() ast.Function {
return ast.Function{ return ast.Function{
ArgTypes: []ast.Type{ast.TypeString, ast.TypeString}, ArgTypes: []ast.Type{ast.TypeList, ast.TypeString},
ReturnType: ast.TypeInt, ReturnType: ast.TypeInt,
Callback: func(args []interface{}) (interface{}, error) { Callback: func(args []interface{}) (interface{}, error) {
haystack := StringList(args[0].(string)).Slice() haystack := args[0].([]ast.Variable)
needle := args[1].(string) needle := args[1].(string)
for index, element := range haystack { for index, element := range haystack {
if needle == element { if needle == element.Value {
return index, nil return index, nil
} }
} }
@ -353,13 +379,28 @@ func interpolationFuncIndex() ast.Function {
// multi-variable values to be joined by some character. // multi-variable values to be joined by some character.
func interpolationFuncJoin() ast.Function { func interpolationFuncJoin() ast.Function {
return ast.Function{ return ast.Function{
ArgTypes: []ast.Type{ast.TypeString, ast.TypeString}, ArgTypes: []ast.Type{ast.TypeString},
Variadic: true,
VariadicType: ast.TypeList,
ReturnType: ast.TypeString, ReturnType: ast.TypeString,
Callback: func(args []interface{}) (interface{}, error) { Callback: func(args []interface{}) (interface{}, error) {
var list []string var list []string
if len(args) < 2 {
return nil, fmt.Errorf("not enough arguments to join()")
}
for _, arg := range args[1:] { for _, arg := range args[1:] {
parts := StringList(arg.(string)).Slice() if parts, ok := arg.(ast.Variable); ok {
list = append(list, parts...) for _, part := range parts.Value.([]ast.Variable) {
list = append(list, part.Value.(string))
}
}
if parts, ok := arg.([]ast.Variable); ok {
for _, part := range parts {
list = append(list, part.Value.(string))
}
}
} }
return strings.Join(list, args[0].(string)), nil return strings.Join(list, args[0].(string)), nil
@ -413,19 +454,20 @@ func interpolationFuncReplace() ast.Function {
func interpolationFuncLength() ast.Function { func interpolationFuncLength() ast.Function {
return ast.Function{ return ast.Function{
ArgTypes: []ast.Type{ast.TypeString}, ArgTypes: []ast.Type{ast.TypeAny},
ReturnType: ast.TypeInt, ReturnType: ast.TypeInt,
Variadic: false, Variadic: false,
Callback: func(args []interface{}) (interface{}, error) { Callback: func(args []interface{}) (interface{}, error) {
if !IsStringList(args[0].(string)) { subject := args[0]
return len(args[0].(string)), nil
switch typedSubject := subject.(type) {
case string:
return len(typedSubject), nil
case []ast.Variable:
return len(typedSubject), nil
} }
length := 0 return 0, fmt.Errorf("arguments to length() must be a string or list")
for _, arg := range args {
length += StringList(arg.(string)).Length()
}
return length, nil
}, },
} }
} }
@ -454,11 +496,12 @@ func interpolationFuncSignum() ast.Function {
func interpolationFuncSplit() ast.Function { func interpolationFuncSplit() ast.Function {
return ast.Function{ return ast.Function{
ArgTypes: []ast.Type{ast.TypeString, ast.TypeString}, ArgTypes: []ast.Type{ast.TypeString, ast.TypeString},
ReturnType: ast.TypeString, ReturnType: ast.TypeList,
Callback: func(args []interface{}) (interface{}, error) { Callback: func(args []interface{}) (interface{}, error) {
sep := args[0].(string) sep := args[0].(string)
s := args[1].(string) s := args[1].(string)
return NewStringList(strings.Split(s, sep)).String(), nil elements := strings.Split(s, sep)
return stringSliceToVariableValue(elements), nil
}, },
} }
} }
@ -495,10 +538,10 @@ func interpolationFuncLookup(vs map[string]ast.Variable) ast.Function {
// wrap if the index is larger than the number of elements in the multi-variable value. // wrap if the index is larger than the number of elements in the multi-variable value.
func interpolationFuncElement() ast.Function { func interpolationFuncElement() ast.Function {
return ast.Function{ return ast.Function{
ArgTypes: []ast.Type{ast.TypeString, ast.TypeString}, ArgTypes: []ast.Type{ast.TypeList, ast.TypeString},
ReturnType: ast.TypeString, ReturnType: ast.TypeString,
Callback: func(args []interface{}) (interface{}, error) { Callback: func(args []interface{}) (interface{}, error) {
list := StringList(args[0].(string)) list := args[0].([]ast.Variable)
index, err := strconv.Atoi(args[1].(string)) index, err := strconv.Atoi(args[1].(string))
if err != nil || index < 0 { if err != nil || index < 0 {
@ -506,7 +549,9 @@ func interpolationFuncElement() ast.Function {
"invalid number for index, got %s", args[1]) "invalid number for index, got %s", args[1])
} }
v := list.Element(index) resolvedIndex := index % len(list)
v := list[resolvedIndex].Value
return v, nil return v, nil
}, },
} }
@ -528,12 +573,8 @@ func interpolationFuncKeys(vs map[string]ast.Variable) ast.Function {
sort.Strings(keys) sort.Strings(keys)
variable, err := hil.InterfaceToVariable(keys) //Keys are guaranteed to be strings
if err != nil { return stringSliceToVariableValue(keys), nil
return nil, err
}
return variable.Value, nil
}, },
} }
} }

View File

@ -17,21 +17,21 @@ func TestInterpolateFuncCompact(t *testing.T) {
// empty string within array // empty string within array
{ {
`${compact(split(",", "a,,b"))}`, `${compact(split(",", "a,,b"))}`,
NewStringList([]string{"a", "b"}).String(), []interface{}{"a", "b"},
false, false,
}, },
// empty string at the end of array // empty string at the end of array
{ {
`${compact(split(",", "a,b,"))}`, `${compact(split(",", "a,b,"))}`,
NewStringList([]string{"a", "b"}).String(), []interface{}{"a", "b"},
false, false,
}, },
// single empty string // single empty string
{ {
`${compact(split(",", ""))}`, `${compact(split(",", ""))}`,
NewStringList([]string{}).String(), []interface{}{},
false, false,
}, },
}, },
@ -174,76 +174,52 @@ func TestInterpolateFuncCoalesce(t *testing.T) {
}) })
} }
func TestInterpolateFuncDeprecatedConcat(t *testing.T) {
testFunction(t, testFunctionConfig{
Cases: []testFunctionCase{
{
`${concat("foo", "bar")}`,
"foobar",
false,
},
{
`${concat("foo")}`,
"foo",
false,
},
{
`${concat()}`,
nil,
true,
},
},
})
}
func TestInterpolateFuncConcat(t *testing.T) { func TestInterpolateFuncConcat(t *testing.T) {
testFunction(t, testFunctionConfig{ testFunction(t, testFunctionConfig{
Cases: []testFunctionCase{ Cases: []testFunctionCase{
// String + list // String + list
{ {
`${concat("a", split(",", "b,c"))}`, `${concat("a", split(",", "b,c"))}`,
NewStringList([]string{"a", "b", "c"}).String(), []interface{}{"a", "b", "c"},
false, false,
}, },
// List + string // List + string
{ {
`${concat(split(",", "a,b"), "c")}`, `${concat(split(",", "a,b"), "c")}`,
NewStringList([]string{"a", "b", "c"}).String(), []interface{}{"a", "b", "c"},
false, false,
}, },
// Single list // Single list
{ {
`${concat(split(",", ",foo,"))}`, `${concat(split(",", ",foo,"))}`,
NewStringList([]string{"", "foo", ""}).String(), []interface{}{"", "foo", ""},
false, false,
}, },
{ {
`${concat(split(",", "a,b,c"))}`, `${concat(split(",", "a,b,c"))}`,
NewStringList([]string{"a", "b", "c"}).String(), []interface{}{"a", "b", "c"},
false, false,
}, },
// Two lists // Two lists
{ {
`${concat(split(",", "a,b,c"), split(",", "d,e"))}`, `${concat(split(",", "a,b,c"), split(",", "d,e"))}`,
NewStringList([]string{"a", "b", "c", "d", "e"}).String(), []interface{}{"a", "b", "c", "d", "e"},
false, false,
}, },
// Two lists with different separators // Two lists with different separators
{ {
`${concat(split(",", "a,b,c"), split(" ", "d e"))}`, `${concat(split(",", "a,b,c"), split(" ", "d e"))}`,
NewStringList([]string{"a", "b", "c", "d", "e"}).String(), []interface{}{"a", "b", "c", "d", "e"},
false, false,
}, },
// More lists // More lists
{ {
`${concat(split(",", "a,b"), split(",", "c,d"), split(",", "e,f"), split(",", "0,1"))}`, `${concat(split(",", "a,b"), split(",", "c,d"), split(",", "e,f"), split(",", "0,1"))}`,
NewStringList([]string{"a", "b", "c", "d", "e", "f", "0", "1"}).String(), []interface{}{"a", "b", "c", "d", "e", "f", "0", "1"},
false, false,
}, },
}, },
@ -338,7 +314,7 @@ func TestInterpolateFuncFormatList(t *testing.T) {
// formatlist applies to each list element in turn // formatlist applies to each list element in turn
{ {
`${formatlist("<%s>", split(",", "A,B"))}`, `${formatlist("<%s>", split(",", "A,B"))}`,
NewStringList([]string{"<A>", "<B>"}).String(), []interface{}{"<A>", "<B>"},
false, false,
}, },
// formatlist repeats scalar elements // formatlist repeats scalar elements
@ -362,7 +338,7 @@ func TestInterpolateFuncFormatList(t *testing.T) {
// Works with lists of length 1 [GH-2240] // Works with lists of length 1 [GH-2240]
{ {
`${formatlist("%s.id", split(",", "demo-rest-elb"))}`, `${formatlist("%s.id", split(",", "demo-rest-elb"))}`,
NewStringList([]string{"demo-rest-elb.id"}).String(), []interface{}{"demo-rest-elb.id"},
false, false,
}, },
}, },
@ -371,6 +347,11 @@ func TestInterpolateFuncFormatList(t *testing.T) {
func TestInterpolateFuncIndex(t *testing.T) { func TestInterpolateFuncIndex(t *testing.T) {
testFunction(t, testFunctionConfig{ testFunction(t, testFunctionConfig{
Vars: map[string]ast.Variable{
"var.list1": interfaceToVariableSwallowError([]string{"notfoo", "stillnotfoo", "bar"}),
"var.list2": interfaceToVariableSwallowError([]string{"foo"}),
"var.list3": interfaceToVariableSwallowError([]string{"foo", "spam", "bar", "eggs"}),
},
Cases: []testFunctionCase{ Cases: []testFunctionCase{
{ {
`${index("test", "")}`, `${index("test", "")}`,
@ -379,22 +360,19 @@ func TestInterpolateFuncIndex(t *testing.T) {
}, },
{ {
fmt.Sprintf(`${index("%s", "foo")}`, `${index(var.list1, "foo")}`,
NewStringList([]string{"notfoo", "stillnotfoo", "bar"}).String()),
nil, nil,
true, true,
}, },
{ {
fmt.Sprintf(`${index("%s", "foo")}`, `${index(var.list2, "foo")}`,
NewStringList([]string{"foo"}).String()),
"0", "0",
false, false,
}, },
{ {
fmt.Sprintf(`${index("%s", "bar")}`, `${index(var.list3, "bar")}`,
NewStringList([]string{"foo", "spam", "bar", "eggs"}).String()),
"2", "2",
false, false,
}, },
@ -404,6 +382,10 @@ func TestInterpolateFuncIndex(t *testing.T) {
func TestInterpolateFuncJoin(t *testing.T) { func TestInterpolateFuncJoin(t *testing.T) {
testFunction(t, testFunctionConfig{ testFunction(t, testFunctionConfig{
Vars: map[string]ast.Variable{
"var.a_list": interfaceToVariableSwallowError([]string{"foo"}),
"var.a_longer_list": interfaceToVariableSwallowError([]string{"foo", "bar", "baz"}),
},
Cases: []testFunctionCase{ Cases: []testFunctionCase{
{ {
`${join(",")}`, `${join(",")}`,
@ -412,24 +394,13 @@ func TestInterpolateFuncJoin(t *testing.T) {
}, },
{ {
fmt.Sprintf(`${join(",", "%s")}`, `${join(",", var.a_list)}`,
NewStringList([]string{"foo"}).String()),
"foo", "foo",
false, false,
}, },
/*
TODO
{ {
`${join(",", "foo", "bar")}`, `${join(".", var.a_longer_list)}`,
"foo,bar",
false,
},
*/
{
fmt.Sprintf(`${join(".", "%s")}`,
NewStringList([]string{"foo", "bar", "baz"}).String()),
"foo.bar.baz", "foo.bar.baz",
false, false,
}, },
@ -632,37 +603,37 @@ func TestInterpolateFuncSplit(t *testing.T) {
{ {
`${split(",", "")}`, `${split(",", "")}`,
NewStringList([]string{""}).String(), []interface{}{""},
false, false,
}, },
{ {
`${split(",", "foo")}`, `${split(",", "foo")}`,
NewStringList([]string{"foo"}).String(), []interface{}{"foo"},
false, false,
}, },
{ {
`${split(",", ",,,")}`, `${split(",", ",,,")}`,
NewStringList([]string{"", "", "", ""}).String(), []interface{}{"", "", "", ""},
false, false,
}, },
{ {
`${split(",", "foo,")}`, `${split(",", "foo,")}`,
NewStringList([]string{"foo", ""}).String(), []interface{}{"foo", ""},
false, false,
}, },
{ {
`${split(",", ",foo,")}`, `${split(",", ",foo,")}`,
NewStringList([]string{"", "foo", ""}).String(), []interface{}{"", "foo", ""},
false, false,
}, },
{ {
`${split(".", "foo.bar.baz")}`, `${split(".", "foo.bar.baz")}`,
NewStringList([]string{"foo", "bar", "baz"}).String(), []interface{}{"foo", "bar", "baz"},
false, false,
}, },
}, },
@ -810,43 +781,47 @@ func TestInterpolateFuncValues(t *testing.T) {
}) })
} }
func interfaceToVariableSwallowError(input interface{}) ast.Variable {
variable, _ := hil.InterfaceToVariable(input)
return variable
}
func TestInterpolateFuncElement(t *testing.T) { func TestInterpolateFuncElement(t *testing.T) {
testFunction(t, testFunctionConfig{ testFunction(t, testFunctionConfig{
Vars: map[string]ast.Variable{
"var.a_list": interfaceToVariableSwallowError([]string{"foo", "baz"}),
"var.a_short_list": interfaceToVariableSwallowError([]string{"foo"}),
},
Cases: []testFunctionCase{ Cases: []testFunctionCase{
{ {
fmt.Sprintf(`${element("%s", "1")}`, `${element(var.a_list, "1")}`,
NewStringList([]string{"foo", "baz"}).String()),
"baz", "baz",
false, false,
}, },
{ {
fmt.Sprintf(`${element("%s", "0")}`, `${element(var.a_short_list, "0")}`,
NewStringList([]string{"foo"}).String()),
"foo", "foo",
false, false,
}, },
// Invalid index should wrap vs. out-of-bounds // Invalid index should wrap vs. out-of-bounds
{ {
fmt.Sprintf(`${element("%s", "2")}`, `${element(var.a_list, "2")}`,
NewStringList([]string{"foo", "baz"}).String()),
"foo", "foo",
false, false,
}, },
// Negative number should fail // Negative number should fail
{ {
fmt.Sprintf(`${element("%s", "-1")}`, `${element(var.a_short_list, "-1")}`,
NewStringList([]string{"foo"}).String()),
nil, nil,
true, true,
}, },
// Too many args // Too many args
{ {
fmt.Sprintf(`${element("%s", "0", "2")}`, `${element(var.a_list, "0", "2")}`,
NewStringList([]string{"foo", "baz"}).String()),
nil, nil,
true, true,
}, },

View File

@ -150,12 +150,15 @@ func (w *interpolationWalker) Primitive(v reflect.Value) error {
// set if it is computed. This behavior is different if we're // set if it is computed. This behavior is different if we're
// splitting (in a SliceElem) or not. // splitting (in a SliceElem) or not.
remove := false remove := false
if w.loc == reflectwalk.SliceElem && IsStringList(replaceVal.(string)) { if w.loc == reflectwalk.SliceElem {
parts := StringList(replaceVal.(string)).Slice() switch typedReplaceVal := replaceVal.(type) {
for _, p := range parts { case string:
if p == UnknownVariableValue { if typedReplaceVal == UnknownVariableValue {
remove = true
}
case []interface{}:
if hasUnknownValue(typedReplaceVal) {
remove = true remove = true
break
} }
} }
} else if replaceVal == UnknownVariableValue { } else if replaceVal == UnknownVariableValue {
@ -226,63 +229,63 @@ func (w *interpolationWalker) replaceCurrent(v reflect.Value) {
} }
} }
func hasUnknownValue(variable []interface{}) bool {
for _, value := range variable {
if strVal, ok := value.(string); ok {
if strVal == UnknownVariableValue {
return true
}
}
}
return false
}
func (w *interpolationWalker) splitSlice() { func (w *interpolationWalker) splitSlice() {
// Get the []interface{} slice so we can do some operations on
// it without dealing with reflection. We'll document each step
// here to be clear.
var s []interface{}
raw := w.cs[len(w.cs)-1] raw := w.cs[len(w.cs)-1]
var s []interface{}
switch v := raw.Interface().(type) { switch v := raw.Interface().(type) {
case []interface{}: case []interface{}:
s = v s = v
case []map[string]interface{}: case []map[string]interface{}:
return return
default:
panic("Unknown kind: " + raw.Kind().String())
} }
// Check if we have any elements that we need to split. If not, then
// just return since we're done.
split := false split := false
for _, v := range s { for _, val := range s {
sv, ok := v.(string) if varVal, ok := val.(ast.Variable); ok && varVal.Type == ast.TypeList {
if !ok { split = true
continue }
} if _, ok := val.([]interface{}); ok {
if IsStringList(sv) {
split = true split = true
break
} }
} }
if !split { if !split {
return return
} }
// Make a new result slice that is twice the capacity to fit our growth. result := make([]interface{}, 0)
result := make([]interface{}, 0, len(s)*2)
// Go over each element of the original slice and start building up
// the resulting slice by splitting where we have to.
for _, v := range s { for _, v := range s {
sv, ok := v.(string) switch val := v.(type) {
if !ok { case ast.Variable:
// Not a string, so just set it switch val.Type {
case ast.TypeList:
elements := val.Value.([]ast.Variable)
for _, element := range elements {
result = append(result, element.Value)
}
default:
result = append(result, val.Value)
}
case []interface{}:
for _, element := range val {
result = append(result, element)
}
default:
result = append(result, v) result = append(result, v)
continue }
} }
if IsStringList(sv) {
for _, p := range StringList(sv).Slice() {
result = append(result, p)
}
continue
}
// Not a string list, so just set it
result = append(result, sv)
}
// Our slice is now done, we have to replace the slice now
// with this new one that we have.
w.replaceCurrent(reflect.ValueOf(result)) w.replaceCurrent(reflect.ValueOf(result))
} }

View File

@ -109,7 +109,7 @@ func TestInterpolationWalker_replace(t *testing.T) {
cases := []struct { cases := []struct {
Input interface{} Input interface{}
Output interface{} Output interface{}
Value string Value interface{}
}{ }{
{ {
Input: map[string]interface{}{ Input: map[string]interface{}{
@ -159,7 +159,7 @@ func TestInterpolationWalker_replace(t *testing.T) {
"bing", "bing",
}, },
}, },
Value: NewStringList([]string{"bar", "baz"}).String(), Value: []interface{}{"bar", "baz"},
}, },
{ {
@ -170,7 +170,7 @@ func TestInterpolationWalker_replace(t *testing.T) {
}, },
}, },
Output: map[string]interface{}{}, Output: map[string]interface{}{},
Value: NewStringList([]string{UnknownVariableValue, "baz"}).String(), Value: []interface{}{UnknownVariableValue, "baz"},
}, },
} }

View File

@ -0,0 +1,4 @@
module "foo" {
source = "./foo"
nodes = [1,2,3]
}

View File

@ -1,4 +1,7 @@
module "foo" { module "foo" {
source = "./foo" source = "./foo"
nodes = [1,2,3] nodes = {
key1 = "value1"
key2 = "value2"
}
} }

View File

@ -305,6 +305,7 @@ func testConfigInterpolate(
t *testing.T, t *testing.T,
raw map[string]interface{}, raw map[string]interface{},
vs map[string]ast.Variable) *terraform.ResourceConfig { vs map[string]ast.Variable) *terraform.ResourceConfig {
rc, err := config.NewRawConfig(raw) rc, err := config.NewRawConfig(raw)
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)

View File

@ -8,6 +8,7 @@ import (
"strconv" "strconv"
"testing" "testing"
"github.com/hashicorp/hil"
"github.com/hashicorp/hil/ast" "github.com/hashicorp/hil/ast"
"github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/hashcode"
@ -123,12 +124,17 @@ func TestValueType_Zero(t *testing.T) {
} }
} }
func interfaceToVariableSwallowError(input interface{}) ast.Variable {
variable, _ := hil.InterfaceToVariable(input)
return variable
}
func TestSchemaMap_Diff(t *testing.T) { func TestSchemaMap_Diff(t *testing.T) {
cases := map[string]struct { cases := map[string]struct {
Schema map[string]*Schema Schema map[string]*Schema
State *terraform.InstanceState State *terraform.InstanceState
Config map[string]interface{} Config map[string]interface{}
ConfigVariables map[string]string ConfigVariables map[string]ast.Variable
Diff *terraform.InstanceDiff Diff *terraform.InstanceDiff
Err bool Err bool
}{ }{
@ -396,8 +402,8 @@ func TestSchemaMap_Diff(t *testing.T) {
"availability_zone": "${var.foo}", "availability_zone": "${var.foo}",
}, },
ConfigVariables: map[string]string{ ConfigVariables: map[string]ast.Variable{
"var.foo": "bar", "var.foo": interfaceToVariableSwallowError("bar"),
}, },
Diff: &terraform.InstanceDiff{ Diff: &terraform.InstanceDiff{
@ -426,8 +432,8 @@ func TestSchemaMap_Diff(t *testing.T) {
"availability_zone": "${var.foo}", "availability_zone": "${var.foo}",
}, },
ConfigVariables: map[string]string{ ConfigVariables: map[string]ast.Variable{
"var.foo": config.UnknownVariableValue, "var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue),
}, },
Diff: &terraform.InstanceDiff{ Diff: &terraform.InstanceDiff{
@ -576,8 +582,8 @@ func TestSchemaMap_Diff(t *testing.T) {
"ports": []interface{}{1, "${var.foo}"}, "ports": []interface{}{1, "${var.foo}"},
}, },
ConfigVariables: map[string]string{ ConfigVariables: map[string]ast.Variable{
"var.foo": config.NewStringList([]string{"2", "5"}).String(), "var.foo": interfaceToVariableSwallowError([]interface{}{"2", "5"}),
}, },
Diff: &terraform.InstanceDiff{ Diff: &terraform.InstanceDiff{
@ -619,9 +625,9 @@ func TestSchemaMap_Diff(t *testing.T) {
"ports": []interface{}{1, "${var.foo}"}, "ports": []interface{}{1, "${var.foo}"},
}, },
ConfigVariables: map[string]string{ ConfigVariables: map[string]ast.Variable{
"var.foo": config.NewStringList([]string{ "var.foo": interfaceToVariableSwallowError([]interface{}{
config.UnknownVariableValue, "5"}).String(), config.UnknownVariableValue, "5"}),
}, },
Diff: &terraform.InstanceDiff{ Diff: &terraform.InstanceDiff{
@ -886,8 +892,8 @@ func TestSchemaMap_Diff(t *testing.T) {
"ports": []interface{}{"${var.foo}", 1}, "ports": []interface{}{"${var.foo}", 1},
}, },
ConfigVariables: map[string]string{ ConfigVariables: map[string]ast.Variable{
"var.foo": config.NewStringList([]string{"2", "5"}).String(), "var.foo": interfaceToVariableSwallowError([]interface{}{"2", "5"}),
}, },
Diff: &terraform.InstanceDiff{ Diff: &terraform.InstanceDiff{
@ -932,9 +938,9 @@ func TestSchemaMap_Diff(t *testing.T) {
"ports": []interface{}{1, "${var.foo}"}, "ports": []interface{}{1, "${var.foo}"},
}, },
ConfigVariables: map[string]string{ ConfigVariables: map[string]ast.Variable{
"var.foo": config.NewStringList([]string{ "var.foo": interfaceToVariableSwallowError([]interface{}{
config.UnknownVariableValue, "5"}).String(), config.UnknownVariableValue, "5"}),
}, },
Diff: &terraform.InstanceDiff{ Diff: &terraform.InstanceDiff{
@ -1603,8 +1609,8 @@ func TestSchemaMap_Diff(t *testing.T) {
"instances": []interface{}{"${var.foo}"}, "instances": []interface{}{"${var.foo}"},
}, },
ConfigVariables: map[string]string{ ConfigVariables: map[string]ast.Variable{
"var.foo": config.UnknownVariableValue, "var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue),
}, },
Diff: &terraform.InstanceDiff{ Diff: &terraform.InstanceDiff{
@ -1654,8 +1660,8 @@ func TestSchemaMap_Diff(t *testing.T) {
}, },
}, },
ConfigVariables: map[string]string{ ConfigVariables: map[string]ast.Variable{
"var.foo": config.UnknownVariableValue, "var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue),
}, },
Diff: &terraform.InstanceDiff{ Diff: &terraform.InstanceDiff{
@ -1720,8 +1726,8 @@ func TestSchemaMap_Diff(t *testing.T) {
}, },
}, },
ConfigVariables: map[string]string{ ConfigVariables: map[string]ast.Variable{
"var.foo": config.UnknownVariableValue, "var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue),
}, },
Diff: &terraform.InstanceDiff{ Diff: &terraform.InstanceDiff{
@ -1787,8 +1793,8 @@ func TestSchemaMap_Diff(t *testing.T) {
}, },
}, },
ConfigVariables: map[string]string{ ConfigVariables: map[string]ast.Variable{
"var.foo": config.UnknownVariableValue, "var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue),
}, },
Diff: &terraform.InstanceDiff{ Diff: &terraform.InstanceDiff{
@ -2134,8 +2140,8 @@ func TestSchemaMap_Diff(t *testing.T) {
"ports": []interface{}{1, "${var.foo}32"}, "ports": []interface{}{1, "${var.foo}32"},
}, },
ConfigVariables: map[string]string{ ConfigVariables: map[string]ast.Variable{
"var.foo": config.UnknownVariableValue, "var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue),
}, },
Diff: &terraform.InstanceDiff{ Diff: &terraform.InstanceDiff{
@ -2403,12 +2409,7 @@ func TestSchemaMap_Diff(t *testing.T) {
} }
if len(tc.ConfigVariables) > 0 { if len(tc.ConfigVariables) > 0 {
vars := make(map[string]ast.Variable) if err := c.Interpolate(tc.ConfigVariables); err != nil {
for k, v := range tc.ConfigVariables {
vars[k] = ast.Variable{Value: v, Type: ast.TypeString}
}
if err := c.Interpolate(vars); err != nil {
t.Fatalf("#%q err: %s", tn, err) t.Fatalf("#%q err: %s", tn, err)
} }
} }
@ -2420,7 +2421,7 @@ func TestSchemaMap_Diff(t *testing.T) {
} }
if !reflect.DeepEqual(tc.Diff, d) { if !reflect.DeepEqual(tc.Diff, d) {
t.Fatalf("#%q:\n\nexpected: %#v\n\ngot:\n\n%#v", tn, tc.Diff, d) t.Fatalf("#%q:\n\nexpected:\n%#v\n\ngot:\n%#v", tn, tc.Diff, d)
} }
} }
} }

View File

@ -225,6 +225,8 @@ func (c *Context) Input(mode InputMode) error {
continue continue
case config.VariableTypeMap: case config.VariableTypeMap:
continue continue
case config.VariableTypeList:
continue
case config.VariableTypeString: case config.VariableTypeString:
// Good! // Good!
default: default:

View File

@ -60,6 +60,10 @@ func (n *EvalTypeCheckVariable) Eval(ctx EvalContext) (interface{}, error) {
continue continue
} }
if proposedValue == config.UnknownVariableValue {
continue
}
switch declaredType { switch declaredType {
case config.VariableTypeString: case config.VariableTypeString:
// This will need actual verification once we aren't dealing with // This will need actual verification once we aren't dealing with
@ -80,6 +84,14 @@ func (n *EvalTypeCheckVariable) Eval(ctx EvalContext) (interface{}, error) {
return nil, fmt.Errorf("variable %s%s should be type %s, got %T", return nil, fmt.Errorf("variable %s%s should be type %s, got %T",
name, modulePathDescription, declaredType.Printable(), proposedValue) name, modulePathDescription, declaredType.Printable(), proposedValue)
} }
case config.VariableTypeList:
switch proposedValue.(type) {
case []interface{}:
continue
default:
return nil, fmt.Errorf("variable %s%s should be type %s, got %T",
name, modulePathDescription, declaredType.Printable(), proposedValue)
}
default: default:
// This will need the actual type substituting when we have more than // This will need the actual type substituting when we have more than
// just strings and maps. // just strings and maps.

View File

@ -63,7 +63,7 @@ func (i *Interpolater) Values(
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid default map value for %s: %v", v.Name, v.Default) return nil, fmt.Errorf("invalid default map value for %s: %v", v.Name, v.Default)
} }
// Potentially TODO(jen20): check against declared type
result[n] = variable result[n] = variable
} }
} }
@ -230,21 +230,26 @@ func (i *Interpolater) valueResourceVar(
return nil return nil
} }
var attr string
var err error
if v.Multi && v.Index == -1 { if v.Multi && v.Index == -1 {
attr, err = i.computeResourceMultiVariable(scope, v) variable, err := i.computeResourceMultiVariable(scope, v)
} else {
attr, err = i.computeResourceVariable(scope, v)
}
if err != nil { if err != nil {
return err return err
} }
if variable == nil {
result[n] = ast.Variable{ return fmt.Errorf("no error reported by variable %q is nil", v.Name)
Value: attr,
Type: ast.TypeString,
} }
result[n] = *variable
} else {
variable, err := i.computeResourceVariable(scope, v)
if err != nil {
return err
}
if variable == nil {
return fmt.Errorf("no error reported by variable %q is nil", v.Name)
}
result[n] = *variable
}
return nil return nil
} }
@ -334,7 +339,7 @@ func (i *Interpolater) valueUserVar(
func (i *Interpolater) computeResourceVariable( func (i *Interpolater) computeResourceVariable(
scope *InterpolationScope, scope *InterpolationScope,
v *config.ResourceVariable) (string, error) { v *config.ResourceVariable) (*ast.Variable, error) {
id := v.ResourceId() id := v.ResourceId()
if v.Multi { if v.Multi {
id = fmt.Sprintf("%s.%d", id, v.Index) id = fmt.Sprintf("%s.%d", id, v.Index)
@ -343,16 +348,18 @@ func (i *Interpolater) computeResourceVariable(
i.StateLock.RLock() i.StateLock.RLock()
defer i.StateLock.RUnlock() defer i.StateLock.RUnlock()
unknownVariable := unknownVariable()
// Get the information about this resource variable, and verify // Get the information about this resource variable, and verify
// that it exists and such. // that it exists and such.
module, _, err := i.resourceVariableInfo(scope, v) module, _, err := i.resourceVariableInfo(scope, v)
if err != nil { if err != nil {
return "", err return nil, err
} }
// If we have no module in the state yet or count, return empty // If we have no module in the state yet or count, return empty
if module == nil || len(module.Resources) == 0 { if module == nil || len(module.Resources) == 0 {
return "", nil return nil, nil
} }
// Get the resource out from the state. We know the state exists // Get the resource out from the state. We know the state exists
@ -374,12 +381,13 @@ func (i *Interpolater) computeResourceVariable(
} }
if attr, ok := r.Primary.Attributes[v.Field]; ok { if attr, ok := r.Primary.Attributes[v.Field]; ok {
return attr, nil return &ast.Variable{Type: ast.TypeString, Value: attr}, nil
} }
// computed list attribute // computed list attribute
if _, ok := r.Primary.Attributes[v.Field+".#"]; ok { if _, ok := r.Primary.Attributes[v.Field+".#"]; ok {
return i.interpolateListAttribute(v.Field, r.Primary.Attributes) variable, err := i.interpolateListAttribute(v.Field, r.Primary.Attributes)
return &variable, err
} }
// At apply time, we can't do the "maybe has it" check below // At apply time, we can't do the "maybe has it" check below
@ -402,13 +410,13 @@ func (i *Interpolater) computeResourceVariable(
// Lists and sets make this // Lists and sets make this
key := fmt.Sprintf("%s.#", strings.Join(parts[:i], ".")) key := fmt.Sprintf("%s.#", strings.Join(parts[:i], "."))
if attr, ok := r.Primary.Attributes[key]; ok { if attr, ok := r.Primary.Attributes[key]; ok {
return attr, nil return &ast.Variable{Type: ast.TypeString, Value: attr}, nil
} }
// Maps make this // Maps make this
key = fmt.Sprintf("%s", strings.Join(parts[:i], ".")) key = fmt.Sprintf("%s", strings.Join(parts[:i], "."))
if attr, ok := r.Primary.Attributes[key]; ok { if attr, ok := r.Primary.Attributes[key]; ok {
return attr, nil return &ast.Variable{Type: ast.TypeString, Value: attr}, nil
} }
} }
} }
@ -418,7 +426,7 @@ MISSING:
// semantic level. If we reached this point and don't have variables, // semantic level. If we reached this point and don't have variables,
// just return the computed value. // just return the computed value.
if scope == nil && scope.Resource == nil { if scope == nil && scope.Resource == nil {
return config.UnknownVariableValue, nil return &unknownVariable, nil
} }
// If the operation is refresh, it isn't an error for a value to // If the operation is refresh, it isn't an error for a value to
@ -432,10 +440,10 @@ MISSING:
// For an input walk, computed values are okay to return because we're only // For an input walk, computed values are okay to return because we're only
// looking for missing variables to prompt the user for. // looking for missing variables to prompt the user for.
if i.Operation == walkRefresh || i.Operation == walkPlanDestroy || i.Operation == walkDestroy || i.Operation == walkInput { if i.Operation == walkRefresh || i.Operation == walkPlanDestroy || i.Operation == walkDestroy || i.Operation == walkInput {
return config.UnknownVariableValue, nil return &unknownVariable, nil
} }
return "", fmt.Errorf( return nil, fmt.Errorf(
"Resource '%s' does not have attribute '%s' "+ "Resource '%s' does not have attribute '%s' "+
"for variable '%s'", "for variable '%s'",
id, id,
@ -445,21 +453,23 @@ MISSING:
func (i *Interpolater) computeResourceMultiVariable( func (i *Interpolater) computeResourceMultiVariable(
scope *InterpolationScope, scope *InterpolationScope,
v *config.ResourceVariable) (string, error) { v *config.ResourceVariable) (*ast.Variable, error) {
i.StateLock.RLock() i.StateLock.RLock()
defer i.StateLock.RUnlock() defer i.StateLock.RUnlock()
unknownVariable := unknownVariable()
// Get the information about this resource variable, and verify // Get the information about this resource variable, and verify
// that it exists and such. // that it exists and such.
module, cr, err := i.resourceVariableInfo(scope, v) module, cr, err := i.resourceVariableInfo(scope, v)
if err != nil { if err != nil {
return "", err return nil, err
} }
// Get the count so we know how many to iterate over // Get the count so we know how many to iterate over
count, err := cr.Count() count, err := cr.Count()
if err != nil { if err != nil {
return "", fmt.Errorf( return nil, fmt.Errorf(
"Error reading %s count: %s", "Error reading %s count: %s",
v.ResourceId(), v.ResourceId(),
err) err)
@ -467,7 +477,7 @@ func (i *Interpolater) computeResourceMultiVariable(
// If we have no module in the state yet or count, return empty // If we have no module in the state yet or count, return empty
if module == nil || len(module.Resources) == 0 || count == 0 { if module == nil || len(module.Resources) == 0 || count == 0 {
return "", nil return &ast.Variable{Type: ast.TypeString, Value: ""}, nil
} }
var values []string var values []string
@ -489,32 +499,37 @@ func (i *Interpolater) computeResourceMultiVariable(
continue continue
} }
attr, ok := r.Primary.Attributes[v.Field] if singleAttr, ok := r.Primary.Attributes[v.Field]; ok {
if !ok { if singleAttr == config.UnknownVariableValue {
return &unknownVariable, nil
}
values = append(values, singleAttr)
continue
}
// computed list attribute // computed list attribute
_, ok := r.Primary.Attributes[v.Field+".#"] _, ok = r.Primary.Attributes[v.Field+".#"]
if !ok { if !ok {
continue continue
} }
attr, err = i.interpolateListAttribute(v.Field, r.Primary.Attributes) multiAttr, err := i.interpolateListAttribute(v.Field, r.Primary.Attributes)
if err != nil { if err != nil {
return "", err return nil, err
}
} }
if config.IsStringList(attr) { if multiAttr == unknownVariable {
for _, s := range config.StringList(attr).Slice() { return &ast.Variable{Type: ast.TypeString, Value: ""}, nil
values = append(values, s)
}
continue
} }
// If any value is unknown, the whole thing is unknown for _, element := range multiAttr.Value.([]ast.Variable) {
if attr == config.UnknownVariableValue { strVal := element.Value.(string)
return config.UnknownVariableValue, nil if strVal == config.UnknownVariableValue {
return &unknownVariable, nil
} }
values = append(values, attr) values = append(values, strVal)
}
} }
if len(values) == 0 { if len(values) == 0 {
@ -529,10 +544,10 @@ func (i *Interpolater) computeResourceMultiVariable(
// For an input walk, computed values are okay to return because we're only // For an input walk, computed values are okay to return because we're only
// looking for missing variables to prompt the user for. // looking for missing variables to prompt the user for.
if i.Operation == walkRefresh || i.Operation == walkPlanDestroy || i.Operation == walkDestroy || i.Operation == walkInput { if i.Operation == walkRefresh || i.Operation == walkPlanDestroy || i.Operation == walkDestroy || i.Operation == walkInput {
return config.UnknownVariableValue, nil return &unknownVariable, nil
} }
return "", fmt.Errorf( return nil, fmt.Errorf(
"Resource '%s' does not have attribute '%s' "+ "Resource '%s' does not have attribute '%s' "+
"for variable '%s'", "for variable '%s'",
v.ResourceId(), v.ResourceId(),
@ -540,12 +555,13 @@ func (i *Interpolater) computeResourceMultiVariable(
v.FullKey()) v.FullKey())
} }
return config.NewStringList(values).String(), nil variable, err := hil.InterfaceToVariable(values)
return &variable, err
} }
func (i *Interpolater) interpolateListAttribute( func (i *Interpolater) interpolateListAttribute(
resourceID string, resourceID string,
attributes map[string]string) (string, error) { attributes map[string]string) (ast.Variable, error) {
attr := attributes[resourceID+".#"] attr := attributes[resourceID+".#"]
log.Printf("[DEBUG] Interpolating computed list attribute %s (%s)", log.Printf("[DEBUG] Interpolating computed list attribute %s (%s)",
@ -556,7 +572,7 @@ func (i *Interpolater) interpolateListAttribute(
// unknown". We must honor that meaning here so computed references can be // unknown". We must honor that meaning here so computed references can be
// treated properly during the plan phase. // treated properly during the plan phase.
if attr == config.UnknownVariableValue { if attr == config.UnknownVariableValue {
return attr, nil return unknownVariable(), nil
} }
// Otherwise we gather the values from the list-like attribute and return // Otherwise we gather the values from the list-like attribute and return
@ -570,7 +586,7 @@ func (i *Interpolater) interpolateListAttribute(
} }
sort.Strings(members) sort.Strings(members)
return config.NewStringList(members).String(), nil return hil.InterfaceToVariable(members)
} }
func (i *Interpolater) resourceVariableInfo( func (i *Interpolater) resourceVariableInfo(

View File

@ -7,6 +7,7 @@ import (
"sync" "sync"
"testing" "testing"
"github.com/hashicorp/hil"
"github.com/hashicorp/hil/ast" "github.com/hashicorp/hil/ast"
"github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config"
) )
@ -210,6 +211,11 @@ func TestInterpolater_resourceVariableMulti(t *testing.T) {
}) })
} }
func interfaceToVariableSwallowError(input interface{}) ast.Variable {
variable, _ := hil.InterfaceToVariable(input)
return variable
}
func TestInterpolator_resourceMultiAttributes(t *testing.T) { func TestInterpolator_resourceMultiAttributes(t *testing.T) {
lock := new(sync.RWMutex) lock := new(sync.RWMutex)
state := &State{ state := &State{
@ -251,31 +257,24 @@ func TestInterpolator_resourceMultiAttributes(t *testing.T) {
Path: rootModulePath, Path: rootModulePath,
} }
name_servers := []string{ name_servers := []interface{}{
"ns-1334.awsdns-38.org", "ns-1334.awsdns-38.org",
"ns-1680.awsdns-18.co.uk", "ns-1680.awsdns-18.co.uk",
"ns-498.awsdns-62.com", "ns-498.awsdns-62.com",
"ns-601.awsdns-11.net", "ns-601.awsdns-11.net",
} }
expectedNameServers := config.NewStringList(name_servers).String()
// More than 1 element // More than 1 element
testInterpolate(t, i, scope, "aws_route53_zone.yada.name_servers", ast.Variable{ testInterpolate(t, i, scope, "aws_route53_zone.yada.name_servers",
Value: expectedNameServers, interfaceToVariableSwallowError(name_servers))
Type: ast.TypeString,
})
// Exactly 1 element // Exactly 1 element
testInterpolate(t, i, scope, "aws_route53_zone.yada.listeners", ast.Variable{ testInterpolate(t, i, scope, "aws_route53_zone.yada.listeners",
Value: config.NewStringList([]string{"red"}).String(), interfaceToVariableSwallowError([]interface{}{"red"}))
Type: ast.TypeString,
})
// Zero elements // Zero elements
testInterpolate(t, i, scope, "aws_route53_zone.yada.nothing", ast.Variable{ testInterpolate(t, i, scope, "aws_route53_zone.yada.nothing",
Value: config.NewStringList([]string{}).String(), interfaceToVariableSwallowError([]interface{}{}))
Type: ast.TypeString,
})
// Maps still need to work // Maps still need to work
testInterpolate(t, i, scope, "aws_route53_zone.yada.tags.Name", ast.Variable{ testInterpolate(t, i, scope, "aws_route53_zone.yada.tags.Name", ast.Variable{
@ -290,7 +289,7 @@ func TestInterpolator_resourceMultiAttributesWithResourceCount(t *testing.T) {
Path: rootModulePath, Path: rootModulePath,
} }
name_servers := []string{ name_servers := []interface{}{
"ns-1334.awsdns-38.org", "ns-1334.awsdns-38.org",
"ns-1680.awsdns-18.co.uk", "ns-1680.awsdns-18.co.uk",
"ns-498.awsdns-62.com", "ns-498.awsdns-62.com",
@ -302,50 +301,38 @@ func TestInterpolator_resourceMultiAttributesWithResourceCount(t *testing.T) {
} }
// More than 1 element // More than 1 element
expectedNameServers := config.NewStringList(name_servers[0:4]).String() testInterpolate(t, i, scope, "aws_route53_zone.terra.0.name_servers",
testInterpolate(t, i, scope, "aws_route53_zone.terra.0.name_servers", ast.Variable{ interfaceToVariableSwallowError(name_servers[0:4]))
Value: expectedNameServers,
Type: ast.TypeString,
})
// More than 1 element in both // More than 1 element in both
expectedNameServers = config.NewStringList(name_servers).String() testInterpolate(t, i, scope, "aws_route53_zone.terra.*.name_servers",
testInterpolate(t, i, scope, "aws_route53_zone.terra.*.name_servers", ast.Variable{ interfaceToVariableSwallowError(name_servers))
Value: expectedNameServers,
Type: ast.TypeString,
})
// Exactly 1 element // Exactly 1 element
testInterpolate(t, i, scope, "aws_route53_zone.terra.0.listeners", ast.Variable{ testInterpolate(t, i, scope, "aws_route53_zone.terra.0.listeners",
Value: config.NewStringList([]string{"red"}).String(), interfaceToVariableSwallowError([]interface{}{"red"}))
Type: ast.TypeString,
})
// Exactly 1 element in both // Exactly 1 element in both
testInterpolate(t, i, scope, "aws_route53_zone.terra.*.listeners", ast.Variable{ testInterpolate(t, i, scope, "aws_route53_zone.terra.*.listeners",
Value: config.NewStringList([]string{"red", "blue"}).String(), interfaceToVariableSwallowError([]interface{}{"red", "blue"}))
Type: ast.TypeString,
})
// Zero elements // Zero elements
testInterpolate(t, i, scope, "aws_route53_zone.terra.0.nothing", ast.Variable{ testInterpolate(t, i, scope, "aws_route53_zone.terra.0.nothing",
Value: config.NewStringList([]string{}).String(), interfaceToVariableSwallowError([]interface{}{}))
Type: ast.TypeString,
})
// Zero + 1 element // Zero + 1 element
testInterpolate(t, i, scope, "aws_route53_zone.terra.*.special", ast.Variable{ testInterpolate(t, i, scope, "aws_route53_zone.terra.*.special",
Value: config.NewStringList([]string{"extra"}).String(), interfaceToVariableSwallowError([]interface{}{"extra"}))
Type: ast.TypeString,
})
// Maps still need to work // Maps still need to work
testInterpolate(t, i, scope, "aws_route53_zone.terra.0.tags.Name", ast.Variable{ testInterpolate(t, i, scope, "aws_route53_zone.terra.0.tags.Name", ast.Variable{
Value: "reindeer", Value: "reindeer",
Type: ast.TypeString, Type: ast.TypeString,
}) })
// Maps still need to work in both // Maps still need to work in both
testInterpolate(t, i, scope, "aws_route53_zone.terra.*.tags.Name", ast.Variable{ testInterpolate(t, i, scope, "aws_route53_zone.terra.*.tags.Name",
Value: config.NewStringList([]string{"reindeer", "white-hart"}).String(), interfaceToVariableSwallowError([]interface{}{"reindeer", "white-hart"}))
Type: ast.TypeString,
})
} }
func TestInterpolator_resourceMultiAttributesComputed(t *testing.T) { func TestInterpolator_resourceMultiAttributesComputed(t *testing.T) {