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 (
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
module "foo" {
|
||||||
|
source = "./foo"
|
||||||
|
nodes = [1,2,3]
|
||||||
|
}
|
|
@ -1,4 +1,7 @@
|
||||||
module "foo" {
|
module "foo" {
|
||||||
source = "./foo"
|
source = "./foo"
|
||||||
nodes = [1,2,3]
|
nodes = {
|
||||||
|
key1 = "value1"
|
||||||
|
key2 = "value2"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue