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:
parent
a49b17147a
commit
f49583d25a
|
@ -161,6 +161,7 @@ type VariableType byte
|
|||
const (
|
||||
VariableTypeUnknown VariableType = iota
|
||||
VariableTypeString
|
||||
VariableTypeList
|
||||
VariableTypeMap
|
||||
)
|
||||
|
||||
|
@ -170,6 +171,8 @@ func (v VariableType) Printable() string {
|
|||
return "string"
|
||||
case VariableTypeMap:
|
||||
return "map"
|
||||
case VariableTypeList:
|
||||
return "list"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
|
@ -351,16 +354,30 @@ func (c *Config) Validate() error {
|
|||
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{})
|
||||
for k, v := range m.RawConfig.Raw {
|
||||
var strVal string
|
||||
if err := mapstructure.WeakDecode(v, &strVal); err != nil {
|
||||
errs = append(errs, fmt.Errorf(
|
||||
"%s: variable %s must be a string value",
|
||||
m.Id(), k))
|
||||
}
|
||||
if err := mapstructure.WeakDecode(v, &strVal); err == nil {
|
||||
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
|
||||
|
@ -721,7 +738,8 @@ func (c *Config) validateVarContextFn(
|
|||
|
||||
if rv.Multi && rv.Index == -1 {
|
||||
*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{
|
||||
"string": VariableTypeString,
|
||||
"map": VariableTypeMap,
|
||||
"list": VariableTypeList,
|
||||
}
|
||||
|
||||
// Type returns the type of variable this is.
|
||||
|
@ -888,9 +907,9 @@ func (v *Variable) inferTypeFromDefault() VariableType {
|
|||
return VariableTypeString
|
||||
}
|
||||
|
||||
var strVal string
|
||||
if err := mapstructure.WeakDecode(v.Default, &strVal); err == nil {
|
||||
v.Default = strVal
|
||||
var s string
|
||||
if err := mapstructure.WeakDecode(v.Default, &s); err == nil {
|
||||
v.Default = s
|
||||
return VariableTypeString
|
||||
}
|
||||
|
||||
|
@ -900,5 +919,11 @@ func (v *Variable) inferTypeFromDefault() VariableType {
|
|||
return VariableTypeMap
|
||||
}
|
||||
|
||||
var l []string
|
||||
if err := mapstructure.WeakDecode(v.Default, &l); err == nil {
|
||||
v.Default = l
|
||||
return VariableTypeList
|
||||
}
|
||||
|
||||
return VariableTypeUnknown
|
||||
}
|
||||
|
|
|
@ -215,8 +215,15 @@ func TestConfigValidate_moduleVarInt(t *testing.T) {
|
|||
|
||||
func TestConfigValidate_moduleVarMap(t *testing.T) {
|
||||
c := testConfig(t, "validate-module-var-map")
|
||||
if err := c.Validate(); err == nil {
|
||||
t.Fatal("should be invalid")
|
||||
if err := c.Validate(); err != nil {
|
||||
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) {
|
||||
c := testConfig(t, "validate-var-default-bad-type")
|
||||
if err := c.Validate(); err == nil {
|
||||
t.Fatal("should not be valid")
|
||||
func TestConfigValidate_varDefaultListType(t *testing.T) {
|
||||
c := testConfig(t, "validate-var-default-list-type")
|
||||
if err := c.Validate(); err != nil {
|
||||
t.Fatal("should be valid: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -284,18 +284,35 @@ func DetectVariables(root ast.Node) ([]InterpolatedVariable, error) {
|
|||
return n
|
||||
}
|
||||
|
||||
vn, ok := n.(*ast.VariableAccess)
|
||||
if !ok {
|
||||
return n
|
||||
}
|
||||
|
||||
switch vn := n.(type) {
|
||||
case *ast.VariableAccess:
|
||||
v, err := NewInterpolatedVariable(vn.Name)
|
||||
if err != nil {
|
||||
resultErr = err
|
||||
return n
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
|
@ -24,6 +23,31 @@ import (
|
|||
"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.
|
||||
func Funcs() 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.
|
||||
func interpolationFuncCompact() ast.Function {
|
||||
return ast.Function{
|
||||
ArgTypes: []ast.Type{ast.TypeString},
|
||||
ReturnType: ast.TypeString,
|
||||
ArgTypes: []ast.Type{ast.TypeList},
|
||||
ReturnType: ast.TypeList,
|
||||
Variadic: false,
|
||||
Callback: func(args []interface{}) (interface{}, error) {
|
||||
if !IsStringList(args[0].(string)) {
|
||||
return args[0].(string), nil
|
||||
inputList := args[0].([]ast.Variable)
|
||||
|
||||
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.
|
||||
func interpolationFuncConcat() ast.Function {
|
||||
return ast.Function{
|
||||
ArgTypes: []ast.Type{ast.TypeString},
|
||||
ReturnType: ast.TypeString,
|
||||
ArgTypes: []ast.Type{ast.TypeAny},
|
||||
ReturnType: ast.TypeList,
|
||||
Variadic: true,
|
||||
VariadicType: ast.TypeString,
|
||||
VariadicType: ast.TypeAny,
|
||||
Callback: func(args []interface{}) (interface{}, error) {
|
||||
var b bytes.Buffer
|
||||
var finalList []string
|
||||
|
||||
var isDeprecated = true
|
||||
var finalListElements []string
|
||||
|
||||
for _, arg := range args {
|
||||
argument := arg.(string)
|
||||
|
||||
if len(argument) == 0 {
|
||||
// Append strings for backward compatibility
|
||||
if argument, ok := arg.(string); ok {
|
||||
finalListElements = append(finalListElements, argument)
|
||||
continue
|
||||
}
|
||||
|
||||
if IsStringList(argument) {
|
||||
isDeprecated = false
|
||||
finalList = append(finalList, StringList(argument).Slice()...)
|
||||
} else {
|
||||
finalList = append(finalList, argument)
|
||||
// Otherwise variables
|
||||
if argument, ok := arg.([]ast.Variable); ok {
|
||||
for _, element := range argument {
|
||||
finalListElements = append(finalListElements, element.Value.(string))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Deprecated concat behaviour
|
||||
b.WriteString(argument)
|
||||
return nil, fmt.Errorf("arguments to concat() must be a string or list")
|
||||
}
|
||||
|
||||
if isDeprecated {
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
return NewStringList(finalList).String(), nil
|
||||
return stringSliceToVariableValue(finalListElements), nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -266,10 +292,10 @@ func interpolationFuncFormat() ast.Function {
|
|||
// string formatting on lists.
|
||||
func interpolationFuncFormatList() ast.Function {
|
||||
return ast.Function{
|
||||
ArgTypes: []ast.Type{ast.TypeString},
|
||||
ArgTypes: []ast.Type{ast.TypeAny},
|
||||
Variadic: true,
|
||||
VariadicType: ast.TypeAny,
|
||||
ReturnType: ast.TypeString,
|
||||
ReturnType: ast.TypeList,
|
||||
Callback: func(args []interface{}) (interface{}, error) {
|
||||
// Make a copy of the variadic part of args
|
||||
// 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).
|
||||
var n int
|
||||
for i := 1; i < len(args); i++ {
|
||||
s, ok := args[i].(string)
|
||||
s, ok := args[i].([]ast.Variable)
|
||||
if !ok {
|
||||
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
|
||||
varargs[i-1] = parts
|
||||
|
@ -325,7 +351,7 @@ func interpolationFuncFormatList() ast.Function {
|
|||
}
|
||||
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
|
||||
func interpolationFuncIndex() ast.Function {
|
||||
return ast.Function{
|
||||
ArgTypes: []ast.Type{ast.TypeString, ast.TypeString},
|
||||
ArgTypes: []ast.Type{ast.TypeList, ast.TypeString},
|
||||
ReturnType: ast.TypeInt,
|
||||
Callback: func(args []interface{}) (interface{}, error) {
|
||||
haystack := StringList(args[0].(string)).Slice()
|
||||
haystack := args[0].([]ast.Variable)
|
||||
needle := args[1].(string)
|
||||
for index, element := range haystack {
|
||||
if needle == element {
|
||||
if needle == element.Value {
|
||||
return index, nil
|
||||
}
|
||||
}
|
||||
|
@ -353,13 +379,28 @@ func interpolationFuncIndex() ast.Function {
|
|||
// multi-variable values to be joined by some character.
|
||||
func interpolationFuncJoin() 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,
|
||||
Callback: func(args []interface{}) (interface{}, error) {
|
||||
var list []string
|
||||
|
||||
if len(args) < 2 {
|
||||
return nil, fmt.Errorf("not enough arguments to join()")
|
||||
}
|
||||
|
||||
for _, arg := range args[1:] {
|
||||
parts := StringList(arg.(string)).Slice()
|
||||
list = append(list, parts...)
|
||||
if parts, ok := arg.(ast.Variable); ok {
|
||||
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
|
||||
|
@ -413,19 +454,20 @@ func interpolationFuncReplace() ast.Function {
|
|||
|
||||
func interpolationFuncLength() ast.Function {
|
||||
return ast.Function{
|
||||
ArgTypes: []ast.Type{ast.TypeString},
|
||||
ArgTypes: []ast.Type{ast.TypeAny},
|
||||
ReturnType: ast.TypeInt,
|
||||
Variadic: false,
|
||||
Callback: func(args []interface{}) (interface{}, error) {
|
||||
if !IsStringList(args[0].(string)) {
|
||||
return len(args[0].(string)), nil
|
||||
subject := args[0]
|
||||
|
||||
switch typedSubject := subject.(type) {
|
||||
case string:
|
||||
return len(typedSubject), nil
|
||||
case []ast.Variable:
|
||||
return len(typedSubject), nil
|
||||
}
|
||||
|
||||
length := 0
|
||||
for _, arg := range args {
|
||||
length += StringList(arg.(string)).Length()
|
||||
}
|
||||
return length, nil
|
||||
return 0, fmt.Errorf("arguments to length() must be a string or list")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -454,11 +496,12 @@ func interpolationFuncSignum() ast.Function {
|
|||
func interpolationFuncSplit() ast.Function {
|
||||
return ast.Function{
|
||||
ArgTypes: []ast.Type{ast.TypeString, ast.TypeString},
|
||||
ReturnType: ast.TypeString,
|
||||
ReturnType: ast.TypeList,
|
||||
Callback: func(args []interface{}) (interface{}, error) {
|
||||
sep := args[0].(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.
|
||||
func interpolationFuncElement() ast.Function {
|
||||
return ast.Function{
|
||||
ArgTypes: []ast.Type{ast.TypeString, ast.TypeString},
|
||||
ArgTypes: []ast.Type{ast.TypeList, ast.TypeString},
|
||||
ReturnType: ast.TypeString,
|
||||
Callback: func(args []interface{}) (interface{}, error) {
|
||||
list := StringList(args[0].(string))
|
||||
list := args[0].([]ast.Variable)
|
||||
|
||||
index, err := strconv.Atoi(args[1].(string))
|
||||
if err != nil || index < 0 {
|
||||
|
@ -506,7 +549,9 @@ func interpolationFuncElement() ast.Function {
|
|||
"invalid number for index, got %s", args[1])
|
||||
}
|
||||
|
||||
v := list.Element(index)
|
||||
resolvedIndex := index % len(list)
|
||||
|
||||
v := list[resolvedIndex].Value
|
||||
return v, nil
|
||||
},
|
||||
}
|
||||
|
@ -528,12 +573,8 @@ func interpolationFuncKeys(vs map[string]ast.Variable) ast.Function {
|
|||
|
||||
sort.Strings(keys)
|
||||
|
||||
variable, err := hil.InterfaceToVariable(keys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return variable.Value, nil
|
||||
//Keys are guaranteed to be strings
|
||||
return stringSliceToVariableValue(keys), nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,21 +17,21 @@ func TestInterpolateFuncCompact(t *testing.T) {
|
|||
// empty string within array
|
||||
{
|
||||
`${compact(split(",", "a,,b"))}`,
|
||||
NewStringList([]string{"a", "b"}).String(),
|
||||
[]interface{}{"a", "b"},
|
||||
false,
|
||||
},
|
||||
|
||||
// empty string at the end of array
|
||||
{
|
||||
`${compact(split(",", "a,b,"))}`,
|
||||
NewStringList([]string{"a", "b"}).String(),
|
||||
[]interface{}{"a", "b"},
|
||||
false,
|
||||
},
|
||||
|
||||
// single empty string
|
||||
{
|
||||
`${compact(split(",", ""))}`,
|
||||
NewStringList([]string{}).String(),
|
||||
[]interface{}{},
|
||||
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) {
|
||||
testFunction(t, testFunctionConfig{
|
||||
Cases: []testFunctionCase{
|
||||
// String + list
|
||||
{
|
||||
`${concat("a", split(",", "b,c"))}`,
|
||||
NewStringList([]string{"a", "b", "c"}).String(),
|
||||
[]interface{}{"a", "b", "c"},
|
||||
false,
|
||||
},
|
||||
|
||||
// List + string
|
||||
{
|
||||
`${concat(split(",", "a,b"), "c")}`,
|
||||
NewStringList([]string{"a", "b", "c"}).String(),
|
||||
[]interface{}{"a", "b", "c"},
|
||||
false,
|
||||
},
|
||||
|
||||
// Single list
|
||||
{
|
||||
`${concat(split(",", ",foo,"))}`,
|
||||
NewStringList([]string{"", "foo", ""}).String(),
|
||||
[]interface{}{"", "foo", ""},
|
||||
false,
|
||||
},
|
||||
{
|
||||
`${concat(split(",", "a,b,c"))}`,
|
||||
NewStringList([]string{"a", "b", "c"}).String(),
|
||||
[]interface{}{"a", "b", "c"},
|
||||
false,
|
||||
},
|
||||
|
||||
// Two lists
|
||||
{
|
||||
`${concat(split(",", "a,b,c"), split(",", "d,e"))}`,
|
||||
NewStringList([]string{"a", "b", "c", "d", "e"}).String(),
|
||||
[]interface{}{"a", "b", "c", "d", "e"},
|
||||
false,
|
||||
},
|
||||
// Two lists with different separators
|
||||
{
|
||||
`${concat(split(",", "a,b,c"), split(" ", "d e"))}`,
|
||||
NewStringList([]string{"a", "b", "c", "d", "e"}).String(),
|
||||
[]interface{}{"a", "b", "c", "d", "e"},
|
||||
false,
|
||||
},
|
||||
|
||||
// More lists
|
||||
{
|
||||
`${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,
|
||||
},
|
||||
},
|
||||
|
@ -338,7 +314,7 @@ func TestInterpolateFuncFormatList(t *testing.T) {
|
|||
// formatlist applies to each list element in turn
|
||||
{
|
||||
`${formatlist("<%s>", split(",", "A,B"))}`,
|
||||
NewStringList([]string{"<A>", "<B>"}).String(),
|
||||
[]interface{}{"<A>", "<B>"},
|
||||
false,
|
||||
},
|
||||
// formatlist repeats scalar elements
|
||||
|
@ -362,7 +338,7 @@ func TestInterpolateFuncFormatList(t *testing.T) {
|
|||
// Works with lists of length 1 [GH-2240]
|
||||
{
|
||||
`${formatlist("%s.id", split(",", "demo-rest-elb"))}`,
|
||||
NewStringList([]string{"demo-rest-elb.id"}).String(),
|
||||
[]interface{}{"demo-rest-elb.id"},
|
||||
false,
|
||||
},
|
||||
},
|
||||
|
@ -371,6 +347,11 @@ func TestInterpolateFuncFormatList(t *testing.T) {
|
|||
|
||||
func TestInterpolateFuncIndex(t *testing.T) {
|
||||
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{
|
||||
{
|
||||
`${index("test", "")}`,
|
||||
|
@ -379,22 +360,19 @@ func TestInterpolateFuncIndex(t *testing.T) {
|
|||
},
|
||||
|
||||
{
|
||||
fmt.Sprintf(`${index("%s", "foo")}`,
|
||||
NewStringList([]string{"notfoo", "stillnotfoo", "bar"}).String()),
|
||||
`${index(var.list1, "foo")}`,
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
fmt.Sprintf(`${index("%s", "foo")}`,
|
||||
NewStringList([]string{"foo"}).String()),
|
||||
`${index(var.list2, "foo")}`,
|
||||
"0",
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
fmt.Sprintf(`${index("%s", "bar")}`,
|
||||
NewStringList([]string{"foo", "spam", "bar", "eggs"}).String()),
|
||||
`${index(var.list3, "bar")}`,
|
||||
"2",
|
||||
false,
|
||||
},
|
||||
|
@ -404,6 +382,10 @@ func TestInterpolateFuncIndex(t *testing.T) {
|
|||
|
||||
func TestInterpolateFuncJoin(t *testing.T) {
|
||||
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{
|
||||
{
|
||||
`${join(",")}`,
|
||||
|
@ -412,24 +394,13 @@ func TestInterpolateFuncJoin(t *testing.T) {
|
|||
},
|
||||
|
||||
{
|
||||
fmt.Sprintf(`${join(",", "%s")}`,
|
||||
NewStringList([]string{"foo"}).String()),
|
||||
`${join(",", var.a_list)}`,
|
||||
"foo",
|
||||
false,
|
||||
},
|
||||
|
||||
/*
|
||||
TODO
|
||||
{
|
||||
`${join(",", "foo", "bar")}`,
|
||||
"foo,bar",
|
||||
false,
|
||||
},
|
||||
*/
|
||||
|
||||
{
|
||||
fmt.Sprintf(`${join(".", "%s")}`,
|
||||
NewStringList([]string{"foo", "bar", "baz"}).String()),
|
||||
`${join(".", var.a_longer_list)}`,
|
||||
"foo.bar.baz",
|
||||
false,
|
||||
},
|
||||
|
@ -632,37 +603,37 @@ func TestInterpolateFuncSplit(t *testing.T) {
|
|||
|
||||
{
|
||||
`${split(",", "")}`,
|
||||
NewStringList([]string{""}).String(),
|
||||
[]interface{}{""},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
`${split(",", "foo")}`,
|
||||
NewStringList([]string{"foo"}).String(),
|
||||
[]interface{}{"foo"},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
`${split(",", ",,,")}`,
|
||||
NewStringList([]string{"", "", "", ""}).String(),
|
||||
[]interface{}{"", "", "", ""},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
`${split(",", "foo,")}`,
|
||||
NewStringList([]string{"foo", ""}).String(),
|
||||
[]interface{}{"foo", ""},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
`${split(",", ",foo,")}`,
|
||||
NewStringList([]string{"", "foo", ""}).String(),
|
||||
[]interface{}{"", "foo", ""},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
`${split(".", "foo.bar.baz")}`,
|
||||
NewStringList([]string{"foo", "bar", "baz"}).String(),
|
||||
[]interface{}{"foo", "bar", "baz"},
|
||||
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) {
|
||||
testFunction(t, testFunctionConfig{
|
||||
Vars: map[string]ast.Variable{
|
||||
"var.a_list": interfaceToVariableSwallowError([]string{"foo", "baz"}),
|
||||
"var.a_short_list": interfaceToVariableSwallowError([]string{"foo"}),
|
||||
},
|
||||
Cases: []testFunctionCase{
|
||||
{
|
||||
fmt.Sprintf(`${element("%s", "1")}`,
|
||||
NewStringList([]string{"foo", "baz"}).String()),
|
||||
`${element(var.a_list, "1")}`,
|
||||
"baz",
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
fmt.Sprintf(`${element("%s", "0")}`,
|
||||
NewStringList([]string{"foo"}).String()),
|
||||
`${element(var.a_short_list, "0")}`,
|
||||
"foo",
|
||||
false,
|
||||
},
|
||||
|
||||
// Invalid index should wrap vs. out-of-bounds
|
||||
{
|
||||
fmt.Sprintf(`${element("%s", "2")}`,
|
||||
NewStringList([]string{"foo", "baz"}).String()),
|
||||
`${element(var.a_list, "2")}`,
|
||||
"foo",
|
||||
false,
|
||||
},
|
||||
|
||||
// Negative number should fail
|
||||
{
|
||||
fmt.Sprintf(`${element("%s", "-1")}`,
|
||||
NewStringList([]string{"foo"}).String()),
|
||||
`${element(var.a_short_list, "-1")}`,
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
|
||||
// Too many args
|
||||
{
|
||||
fmt.Sprintf(`${element("%s", "0", "2")}`,
|
||||
NewStringList([]string{"foo", "baz"}).String()),
|
||||
`${element(var.a_list, "0", "2")}`,
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
|
|
|
@ -150,12 +150,15 @@ func (w *interpolationWalker) Primitive(v reflect.Value) error {
|
|||
// set if it is computed. This behavior is different if we're
|
||||
// splitting (in a SliceElem) or not.
|
||||
remove := false
|
||||
if w.loc == reflectwalk.SliceElem && IsStringList(replaceVal.(string)) {
|
||||
parts := StringList(replaceVal.(string)).Slice()
|
||||
for _, p := range parts {
|
||||
if p == UnknownVariableValue {
|
||||
if w.loc == reflectwalk.SliceElem {
|
||||
switch typedReplaceVal := replaceVal.(type) {
|
||||
case string:
|
||||
if typedReplaceVal == UnknownVariableValue {
|
||||
remove = true
|
||||
}
|
||||
case []interface{}:
|
||||
if hasUnknownValue(typedReplaceVal) {
|
||||
remove = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} 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() {
|
||||
// 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]
|
||||
|
||||
var s []interface{}
|
||||
switch v := raw.Interface().(type) {
|
||||
case []interface{}:
|
||||
s = v
|
||||
case []map[string]interface{}:
|
||||
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
|
||||
for _, v := range s {
|
||||
sv, ok := v.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if IsStringList(sv) {
|
||||
for _, val := range s {
|
||||
if varVal, ok := val.(ast.Variable); ok && varVal.Type == ast.TypeList {
|
||||
split = true
|
||||
}
|
||||
if _, ok := val.([]interface{}); ok {
|
||||
split = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !split {
|
||||
return
|
||||
}
|
||||
|
||||
// Make a new result slice that is twice the capacity to fit our growth.
|
||||
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.
|
||||
result := make([]interface{}, 0)
|
||||
for _, v := range s {
|
||||
sv, ok := v.(string)
|
||||
if !ok {
|
||||
// Not a string, so just set it
|
||||
switch val := v.(type) {
|
||||
case ast.Variable:
|
||||
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)
|
||||
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))
|
||||
}
|
||||
|
|
|
@ -109,7 +109,7 @@ func TestInterpolationWalker_replace(t *testing.T) {
|
|||
cases := []struct {
|
||||
Input interface{}
|
||||
Output interface{}
|
||||
Value string
|
||||
Value interface{}
|
||||
}{
|
||||
{
|
||||
Input: map[string]interface{}{
|
||||
|
@ -159,7 +159,7 @@ func TestInterpolationWalker_replace(t *testing.T) {
|
|||
"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{}{},
|
||||
Value: NewStringList([]string{UnknownVariableValue, "baz"}).String(),
|
||||
Value: []interface{}{UnknownVariableValue, "baz"},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
module "foo" {
|
||||
source = "./foo"
|
||||
nodes = [1,2,3]
|
||||
}
|
|
@ -1,4 +1,7 @@
|
|||
module "foo" {
|
||||
source = "./foo"
|
||||
nodes = [1,2,3]
|
||||
nodes = {
|
||||
key1 = "value1"
|
||||
key2 = "value2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -305,6 +305,7 @@ func testConfigInterpolate(
|
|||
t *testing.T,
|
||||
raw map[string]interface{},
|
||||
vs map[string]ast.Variable) *terraform.ResourceConfig {
|
||||
|
||||
rc, err := config.NewRawConfig(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/hil"
|
||||
"github.com/hashicorp/hil/ast"
|
||||
"github.com/hashicorp/terraform/config"
|
||||
"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) {
|
||||
cases := map[string]struct {
|
||||
Schema map[string]*Schema
|
||||
State *terraform.InstanceState
|
||||
Config map[string]interface{}
|
||||
ConfigVariables map[string]string
|
||||
ConfigVariables map[string]ast.Variable
|
||||
Diff *terraform.InstanceDiff
|
||||
Err bool
|
||||
}{
|
||||
|
@ -396,8 +402,8 @@ func TestSchemaMap_Diff(t *testing.T) {
|
|||
"availability_zone": "${var.foo}",
|
||||
},
|
||||
|
||||
ConfigVariables: map[string]string{
|
||||
"var.foo": "bar",
|
||||
ConfigVariables: map[string]ast.Variable{
|
||||
"var.foo": interfaceToVariableSwallowError("bar"),
|
||||
},
|
||||
|
||||
Diff: &terraform.InstanceDiff{
|
||||
|
@ -426,8 +432,8 @@ func TestSchemaMap_Diff(t *testing.T) {
|
|||
"availability_zone": "${var.foo}",
|
||||
},
|
||||
|
||||
ConfigVariables: map[string]string{
|
||||
"var.foo": config.UnknownVariableValue,
|
||||
ConfigVariables: map[string]ast.Variable{
|
||||
"var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue),
|
||||
},
|
||||
|
||||
Diff: &terraform.InstanceDiff{
|
||||
|
@ -576,8 +582,8 @@ func TestSchemaMap_Diff(t *testing.T) {
|
|||
"ports": []interface{}{1, "${var.foo}"},
|
||||
},
|
||||
|
||||
ConfigVariables: map[string]string{
|
||||
"var.foo": config.NewStringList([]string{"2", "5"}).String(),
|
||||
ConfigVariables: map[string]ast.Variable{
|
||||
"var.foo": interfaceToVariableSwallowError([]interface{}{"2", "5"}),
|
||||
},
|
||||
|
||||
Diff: &terraform.InstanceDiff{
|
||||
|
@ -619,9 +625,9 @@ func TestSchemaMap_Diff(t *testing.T) {
|
|||
"ports": []interface{}{1, "${var.foo}"},
|
||||
},
|
||||
|
||||
ConfigVariables: map[string]string{
|
||||
"var.foo": config.NewStringList([]string{
|
||||
config.UnknownVariableValue, "5"}).String(),
|
||||
ConfigVariables: map[string]ast.Variable{
|
||||
"var.foo": interfaceToVariableSwallowError([]interface{}{
|
||||
config.UnknownVariableValue, "5"}),
|
||||
},
|
||||
|
||||
Diff: &terraform.InstanceDiff{
|
||||
|
@ -886,8 +892,8 @@ func TestSchemaMap_Diff(t *testing.T) {
|
|||
"ports": []interface{}{"${var.foo}", 1},
|
||||
},
|
||||
|
||||
ConfigVariables: map[string]string{
|
||||
"var.foo": config.NewStringList([]string{"2", "5"}).String(),
|
||||
ConfigVariables: map[string]ast.Variable{
|
||||
"var.foo": interfaceToVariableSwallowError([]interface{}{"2", "5"}),
|
||||
},
|
||||
|
||||
Diff: &terraform.InstanceDiff{
|
||||
|
@ -932,9 +938,9 @@ func TestSchemaMap_Diff(t *testing.T) {
|
|||
"ports": []interface{}{1, "${var.foo}"},
|
||||
},
|
||||
|
||||
ConfigVariables: map[string]string{
|
||||
"var.foo": config.NewStringList([]string{
|
||||
config.UnknownVariableValue, "5"}).String(),
|
||||
ConfigVariables: map[string]ast.Variable{
|
||||
"var.foo": interfaceToVariableSwallowError([]interface{}{
|
||||
config.UnknownVariableValue, "5"}),
|
||||
},
|
||||
|
||||
Diff: &terraform.InstanceDiff{
|
||||
|
@ -1603,8 +1609,8 @@ func TestSchemaMap_Diff(t *testing.T) {
|
|||
"instances": []interface{}{"${var.foo}"},
|
||||
},
|
||||
|
||||
ConfigVariables: map[string]string{
|
||||
"var.foo": config.UnknownVariableValue,
|
||||
ConfigVariables: map[string]ast.Variable{
|
||||
"var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue),
|
||||
},
|
||||
|
||||
Diff: &terraform.InstanceDiff{
|
||||
|
@ -1654,8 +1660,8 @@ func TestSchemaMap_Diff(t *testing.T) {
|
|||
},
|
||||
},
|
||||
|
||||
ConfigVariables: map[string]string{
|
||||
"var.foo": config.UnknownVariableValue,
|
||||
ConfigVariables: map[string]ast.Variable{
|
||||
"var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue),
|
||||
},
|
||||
|
||||
Diff: &terraform.InstanceDiff{
|
||||
|
@ -1720,8 +1726,8 @@ func TestSchemaMap_Diff(t *testing.T) {
|
|||
},
|
||||
},
|
||||
|
||||
ConfigVariables: map[string]string{
|
||||
"var.foo": config.UnknownVariableValue,
|
||||
ConfigVariables: map[string]ast.Variable{
|
||||
"var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue),
|
||||
},
|
||||
|
||||
Diff: &terraform.InstanceDiff{
|
||||
|
@ -1787,8 +1793,8 @@ func TestSchemaMap_Diff(t *testing.T) {
|
|||
},
|
||||
},
|
||||
|
||||
ConfigVariables: map[string]string{
|
||||
"var.foo": config.UnknownVariableValue,
|
||||
ConfigVariables: map[string]ast.Variable{
|
||||
"var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue),
|
||||
},
|
||||
|
||||
Diff: &terraform.InstanceDiff{
|
||||
|
@ -2134,8 +2140,8 @@ func TestSchemaMap_Diff(t *testing.T) {
|
|||
"ports": []interface{}{1, "${var.foo}32"},
|
||||
},
|
||||
|
||||
ConfigVariables: map[string]string{
|
||||
"var.foo": config.UnknownVariableValue,
|
||||
ConfigVariables: map[string]ast.Variable{
|
||||
"var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue),
|
||||
},
|
||||
|
||||
Diff: &terraform.InstanceDiff{
|
||||
|
@ -2403,12 +2409,7 @@ func TestSchemaMap_Diff(t *testing.T) {
|
|||
}
|
||||
|
||||
if len(tc.ConfigVariables) > 0 {
|
||||
vars := make(map[string]ast.Variable)
|
||||
for k, v := range tc.ConfigVariables {
|
||||
vars[k] = ast.Variable{Value: v, Type: ast.TypeString}
|
||||
}
|
||||
|
||||
if err := c.Interpolate(vars); err != nil {
|
||||
if err := c.Interpolate(tc.ConfigVariables); err != nil {
|
||||
t.Fatalf("#%q err: %s", tn, err)
|
||||
}
|
||||
}
|
||||
|
@ -2420,7 +2421,7 @@ func TestSchemaMap_Diff(t *testing.T) {
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -225,6 +225,8 @@ func (c *Context) Input(mode InputMode) error {
|
|||
continue
|
||||
case config.VariableTypeMap:
|
||||
continue
|
||||
case config.VariableTypeList:
|
||||
continue
|
||||
case config.VariableTypeString:
|
||||
// Good!
|
||||
default:
|
||||
|
|
|
@ -60,6 +60,10 @@ func (n *EvalTypeCheckVariable) Eval(ctx EvalContext) (interface{}, error) {
|
|||
continue
|
||||
}
|
||||
|
||||
if proposedValue == config.UnknownVariableValue {
|
||||
continue
|
||||
}
|
||||
|
||||
switch declaredType {
|
||||
case config.VariableTypeString:
|
||||
// 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",
|
||||
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:
|
||||
// This will need the actual type substituting when we have more than
|
||||
// just strings and maps.
|
||||
|
|
|
@ -63,7 +63,7 @@ func (i *Interpolater) Values(
|
|||
if err != nil {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -230,21 +230,26 @@ func (i *Interpolater) valueResourceVar(
|
|||
return nil
|
||||
}
|
||||
|
||||
var attr string
|
||||
var err error
|
||||
if v.Multi && v.Index == -1 {
|
||||
attr, err = i.computeResourceMultiVariable(scope, v)
|
||||
} else {
|
||||
attr, err = i.computeResourceVariable(scope, v)
|
||||
}
|
||||
variable, err := i.computeResourceMultiVariable(scope, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result[n] = ast.Variable{
|
||||
Value: attr,
|
||||
Type: ast.TypeString,
|
||||
if variable == nil {
|
||||
return fmt.Errorf("no error reported by variable %q is nil", v.Name)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -334,7 +339,7 @@ func (i *Interpolater) valueUserVar(
|
|||
|
||||
func (i *Interpolater) computeResourceVariable(
|
||||
scope *InterpolationScope,
|
||||
v *config.ResourceVariable) (string, error) {
|
||||
v *config.ResourceVariable) (*ast.Variable, error) {
|
||||
id := v.ResourceId()
|
||||
if v.Multi {
|
||||
id = fmt.Sprintf("%s.%d", id, v.Index)
|
||||
|
@ -343,16 +348,18 @@ func (i *Interpolater) computeResourceVariable(
|
|||
i.StateLock.RLock()
|
||||
defer i.StateLock.RUnlock()
|
||||
|
||||
unknownVariable := unknownVariable()
|
||||
|
||||
// Get the information about this resource variable, and verify
|
||||
// that it exists and such.
|
||||
module, _, err := i.resourceVariableInfo(scope, v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If we have no module in the state yet or count, return empty
|
||||
if module == nil || len(module.Resources) == 0 {
|
||||
return "", nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return attr, nil
|
||||
return &ast.Variable{Type: ast.TypeString, Value: attr}, nil
|
||||
}
|
||||
|
||||
// computed list attribute
|
||||
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
|
||||
|
@ -402,13 +410,13 @@ func (i *Interpolater) computeResourceVariable(
|
|||
// Lists and sets make this
|
||||
key := fmt.Sprintf("%s.#", strings.Join(parts[:i], "."))
|
||||
if attr, ok := r.Primary.Attributes[key]; ok {
|
||||
return attr, nil
|
||||
return &ast.Variable{Type: ast.TypeString, Value: attr}, nil
|
||||
}
|
||||
|
||||
// Maps make this
|
||||
key = fmt.Sprintf("%s", strings.Join(parts[:i], "."))
|
||||
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,
|
||||
// just return the computed value.
|
||||
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
|
||||
|
@ -432,10 +440,10 @@ MISSING:
|
|||
// For an input walk, computed values are okay to return because we're only
|
||||
// looking for missing variables to prompt the user for.
|
||||
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' "+
|
||||
"for variable '%s'",
|
||||
id,
|
||||
|
@ -445,21 +453,23 @@ MISSING:
|
|||
|
||||
func (i *Interpolater) computeResourceMultiVariable(
|
||||
scope *InterpolationScope,
|
||||
v *config.ResourceVariable) (string, error) {
|
||||
v *config.ResourceVariable) (*ast.Variable, error) {
|
||||
i.StateLock.RLock()
|
||||
defer i.StateLock.RUnlock()
|
||||
|
||||
unknownVariable := unknownVariable()
|
||||
|
||||
// Get the information about this resource variable, and verify
|
||||
// that it exists and such.
|
||||
module, cr, err := i.resourceVariableInfo(scope, v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get the count so we know how many to iterate over
|
||||
count, err := cr.Count()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf(
|
||||
return nil, fmt.Errorf(
|
||||
"Error reading %s count: %s",
|
||||
v.ResourceId(),
|
||||
err)
|
||||
|
@ -467,7 +477,7 @@ func (i *Interpolater) computeResourceMultiVariable(
|
|||
|
||||
// If we have no module in the state yet or count, return empty
|
||||
if module == nil || len(module.Resources) == 0 || count == 0 {
|
||||
return "", nil
|
||||
return &ast.Variable{Type: ast.TypeString, Value: ""}, nil
|
||||
}
|
||||
|
||||
var values []string
|
||||
|
@ -489,32 +499,37 @@ func (i *Interpolater) computeResourceMultiVariable(
|
|||
continue
|
||||
}
|
||||
|
||||
attr, ok := r.Primary.Attributes[v.Field]
|
||||
if !ok {
|
||||
if singleAttr, ok := r.Primary.Attributes[v.Field]; ok {
|
||||
if singleAttr == config.UnknownVariableValue {
|
||||
return &unknownVariable, nil
|
||||
}
|
||||
|
||||
values = append(values, singleAttr)
|
||||
continue
|
||||
}
|
||||
|
||||
// computed list attribute
|
||||
_, ok := r.Primary.Attributes[v.Field+".#"]
|
||||
_, ok = r.Primary.Attributes[v.Field+".#"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
attr, err = i.interpolateListAttribute(v.Field, r.Primary.Attributes)
|
||||
multiAttr, err := i.interpolateListAttribute(v.Field, r.Primary.Attributes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if config.IsStringList(attr) {
|
||||
for _, s := range config.StringList(attr).Slice() {
|
||||
values = append(values, s)
|
||||
}
|
||||
continue
|
||||
if multiAttr == unknownVariable {
|
||||
return &ast.Variable{Type: ast.TypeString, Value: ""}, nil
|
||||
}
|
||||
|
||||
// If any value is unknown, the whole thing is unknown
|
||||
if attr == config.UnknownVariableValue {
|
||||
return config.UnknownVariableValue, nil
|
||||
for _, element := range multiAttr.Value.([]ast.Variable) {
|
||||
strVal := element.Value.(string)
|
||||
if strVal == config.UnknownVariableValue {
|
||||
return &unknownVariable, nil
|
||||
}
|
||||
|
||||
values = append(values, attr)
|
||||
values = append(values, strVal)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// looking for missing variables to prompt the user for.
|
||||
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' "+
|
||||
"for variable '%s'",
|
||||
v.ResourceId(),
|
||||
|
@ -540,12 +555,13 @@ func (i *Interpolater) computeResourceMultiVariable(
|
|||
v.FullKey())
|
||||
}
|
||||
|
||||
return config.NewStringList(values).String(), nil
|
||||
variable, err := hil.InterfaceToVariable(values)
|
||||
return &variable, err
|
||||
}
|
||||
|
||||
func (i *Interpolater) interpolateListAttribute(
|
||||
resourceID string,
|
||||
attributes map[string]string) (string, error) {
|
||||
attributes map[string]string) (ast.Variable, error) {
|
||||
|
||||
attr := attributes[resourceID+".#"]
|
||||
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
|
||||
// treated properly during the plan phase.
|
||||
if attr == config.UnknownVariableValue {
|
||||
return attr, nil
|
||||
return unknownVariable(), nil
|
||||
}
|
||||
|
||||
// Otherwise we gather the values from the list-like attribute and return
|
||||
|
@ -570,7 +586,7 @@ func (i *Interpolater) interpolateListAttribute(
|
|||
}
|
||||
|
||||
sort.Strings(members)
|
||||
return config.NewStringList(members).String(), nil
|
||||
return hil.InterfaceToVariable(members)
|
||||
}
|
||||
|
||||
func (i *Interpolater) resourceVariableInfo(
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/hil"
|
||||
"github.com/hashicorp/hil/ast"
|
||||
"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) {
|
||||
lock := new(sync.RWMutex)
|
||||
state := &State{
|
||||
|
@ -251,31 +257,24 @@ func TestInterpolator_resourceMultiAttributes(t *testing.T) {
|
|||
Path: rootModulePath,
|
||||
}
|
||||
|
||||
name_servers := []string{
|
||||
name_servers := []interface{}{
|
||||
"ns-1334.awsdns-38.org",
|
||||
"ns-1680.awsdns-18.co.uk",
|
||||
"ns-498.awsdns-62.com",
|
||||
"ns-601.awsdns-11.net",
|
||||
}
|
||||
expectedNameServers := config.NewStringList(name_servers).String()
|
||||
|
||||
// More than 1 element
|
||||
testInterpolate(t, i, scope, "aws_route53_zone.yada.name_servers", ast.Variable{
|
||||
Value: expectedNameServers,
|
||||
Type: ast.TypeString,
|
||||
})
|
||||
testInterpolate(t, i, scope, "aws_route53_zone.yada.name_servers",
|
||||
interfaceToVariableSwallowError(name_servers))
|
||||
|
||||
// Exactly 1 element
|
||||
testInterpolate(t, i, scope, "aws_route53_zone.yada.listeners", ast.Variable{
|
||||
Value: config.NewStringList([]string{"red"}).String(),
|
||||
Type: ast.TypeString,
|
||||
})
|
||||
testInterpolate(t, i, scope, "aws_route53_zone.yada.listeners",
|
||||
interfaceToVariableSwallowError([]interface{}{"red"}))
|
||||
|
||||
// Zero elements
|
||||
testInterpolate(t, i, scope, "aws_route53_zone.yada.nothing", ast.Variable{
|
||||
Value: config.NewStringList([]string{}).String(),
|
||||
Type: ast.TypeString,
|
||||
})
|
||||
testInterpolate(t, i, scope, "aws_route53_zone.yada.nothing",
|
||||
interfaceToVariableSwallowError([]interface{}{}))
|
||||
|
||||
// Maps still need to work
|
||||
testInterpolate(t, i, scope, "aws_route53_zone.yada.tags.Name", ast.Variable{
|
||||
|
@ -290,7 +289,7 @@ func TestInterpolator_resourceMultiAttributesWithResourceCount(t *testing.T) {
|
|||
Path: rootModulePath,
|
||||
}
|
||||
|
||||
name_servers := []string{
|
||||
name_servers := []interface{}{
|
||||
"ns-1334.awsdns-38.org",
|
||||
"ns-1680.awsdns-18.co.uk",
|
||||
"ns-498.awsdns-62.com",
|
||||
|
@ -302,50 +301,38 @@ func TestInterpolator_resourceMultiAttributesWithResourceCount(t *testing.T) {
|
|||
}
|
||||
|
||||
// More than 1 element
|
||||
expectedNameServers := config.NewStringList(name_servers[0:4]).String()
|
||||
testInterpolate(t, i, scope, "aws_route53_zone.terra.0.name_servers", ast.Variable{
|
||||
Value: expectedNameServers,
|
||||
Type: ast.TypeString,
|
||||
})
|
||||
testInterpolate(t, i, scope, "aws_route53_zone.terra.0.name_servers",
|
||||
interfaceToVariableSwallowError(name_servers[0:4]))
|
||||
|
||||
// More than 1 element in both
|
||||
expectedNameServers = config.NewStringList(name_servers).String()
|
||||
testInterpolate(t, i, scope, "aws_route53_zone.terra.*.name_servers", ast.Variable{
|
||||
Value: expectedNameServers,
|
||||
Type: ast.TypeString,
|
||||
})
|
||||
testInterpolate(t, i, scope, "aws_route53_zone.terra.*.name_servers",
|
||||
interfaceToVariableSwallowError(name_servers))
|
||||
|
||||
// Exactly 1 element
|
||||
testInterpolate(t, i, scope, "aws_route53_zone.terra.0.listeners", ast.Variable{
|
||||
Value: config.NewStringList([]string{"red"}).String(),
|
||||
Type: ast.TypeString,
|
||||
})
|
||||
testInterpolate(t, i, scope, "aws_route53_zone.terra.0.listeners",
|
||||
interfaceToVariableSwallowError([]interface{}{"red"}))
|
||||
|
||||
// Exactly 1 element in both
|
||||
testInterpolate(t, i, scope, "aws_route53_zone.terra.*.listeners", ast.Variable{
|
||||
Value: config.NewStringList([]string{"red", "blue"}).String(),
|
||||
Type: ast.TypeString,
|
||||
})
|
||||
testInterpolate(t, i, scope, "aws_route53_zone.terra.*.listeners",
|
||||
interfaceToVariableSwallowError([]interface{}{"red", "blue"}))
|
||||
|
||||
// Zero elements
|
||||
testInterpolate(t, i, scope, "aws_route53_zone.terra.0.nothing", ast.Variable{
|
||||
Value: config.NewStringList([]string{}).String(),
|
||||
Type: ast.TypeString,
|
||||
})
|
||||
testInterpolate(t, i, scope, "aws_route53_zone.terra.0.nothing",
|
||||
interfaceToVariableSwallowError([]interface{}{}))
|
||||
|
||||
// Zero + 1 element
|
||||
testInterpolate(t, i, scope, "aws_route53_zone.terra.*.special", ast.Variable{
|
||||
Value: config.NewStringList([]string{"extra"}).String(),
|
||||
Type: ast.TypeString,
|
||||
})
|
||||
testInterpolate(t, i, scope, "aws_route53_zone.terra.*.special",
|
||||
interfaceToVariableSwallowError([]interface{}{"extra"}))
|
||||
|
||||
// Maps still need to work
|
||||
testInterpolate(t, i, scope, "aws_route53_zone.terra.0.tags.Name", ast.Variable{
|
||||
Value: "reindeer",
|
||||
Type: ast.TypeString,
|
||||
})
|
||||
|
||||
// Maps still need to work in both
|
||||
testInterpolate(t, i, scope, "aws_route53_zone.terra.*.tags.Name", ast.Variable{
|
||||
Value: config.NewStringList([]string{"reindeer", "white-hart"}).String(),
|
||||
Type: ast.TypeString,
|
||||
})
|
||||
testInterpolate(t, i, scope, "aws_route53_zone.terra.*.tags.Name",
|
||||
interfaceToVariableSwallowError([]interface{}{"reindeer", "white-hart"}))
|
||||
}
|
||||
|
||||
func TestInterpolator_resourceMultiAttributesComputed(t *testing.T) {
|
||||
|
|
Loading…
Reference in New Issue