Merge pull request #797 from hashicorp/f-stronger-types

Force variables to be typed (internally)
This commit is contained in:
Mitchell Hashimoto 2015-01-14 15:30:38 -08:00
commit d3c0543bf3
17 changed files with 510 additions and 94 deletions

View File

@ -81,7 +81,7 @@ func interpolationFuncJoin() lang.Function {
// interpolationFuncLookup implements the "lookup" function that allows
// dynamic lookups of map types within a Terraform configuration.
func interpolationFuncLookup(vs map[string]string) lang.Function {
func interpolationFuncLookup(vs map[string]lang.Variable) lang.Function {
return lang.Function{
ArgTypes: []ast.Type{ast.TypeString, ast.TypeString},
ReturnType: ast.TypeString,
@ -93,8 +93,13 @@ func interpolationFuncLookup(vs map[string]string) lang.Function {
"lookup in '%s' failed to find '%s'",
args[0].(string), args[1].(string))
}
if v.Type != ast.TypeString {
return "", fmt.Errorf(
"lookup in '%s' for '%s' has bad type %s",
args[0].(string), args[1].(string), v.Type)
}
return v, nil
return v.Value.(string), nil
},
}
}

View File

@ -8,6 +8,7 @@ import (
"testing"
"github.com/hashicorp/terraform/config/lang"
"github.com/hashicorp/terraform/config/lang/ast"
)
func TestInterpolateFuncConcat(t *testing.T) {
@ -108,7 +109,12 @@ func TestInterpolateFuncJoin(t *testing.T) {
func TestInterpolateFuncLookup(t *testing.T) {
testFunction(t, testFunctionConfig{
Vars: map[string]string{"var.foo.bar": "baz"},
Vars: map[string]lang.Variable{
"var.foo.bar": lang.Variable{
Value: "baz",
Type: ast.TypeString,
},
},
Cases: []testFunctionCase{
{
`${lookup("foo", "bar")}`,
@ -170,7 +176,7 @@ func TestInterpolateFuncElement(t *testing.T) {
type testFunctionConfig struct {
Cases []testFunctionCase
Vars map[string]string
Vars map[string]lang.Variable
}
type testFunctionCase struct {

View File

@ -1,6 +1,7 @@
package ast
import (
"bytes"
"fmt"
)
@ -26,3 +27,12 @@ func (n *Concat) Pos() Pos {
func (n *Concat) GoString() string {
return fmt.Sprintf("*%#v", *n)
}
func (n *Concat) String() string {
var b bytes.Buffer
for _, expr := range n.Exprs {
b.WriteString(fmt.Sprintf("%s", expr))
}
return b.String()
}

42
config/lang/builtins.go Normal file
View File

@ -0,0 +1,42 @@
package lang
import (
"strconv"
"github.com/hashicorp/terraform/config/lang/ast"
)
// NOTE: All builtins are tested in engine_test.go
func registerBuiltins(scope *Scope) {
if scope.FuncMap == nil {
scope.FuncMap = make(map[string]Function)
}
scope.FuncMap["__builtin_IntToString"] = builtinIntToString()
scope.FuncMap["__builtin_StringToInt"] = builtinStringToInt()
}
func builtinIntToString() Function {
return Function{
ArgTypes: []ast.Type{ast.TypeInt},
ReturnType: ast.TypeString,
Callback: func(args []interface{}) (interface{}, error) {
return strconv.FormatInt(int64(args[0].(int)), 10), nil
},
}
}
func builtinStringToInt() Function {
return Function{
ArgTypes: []ast.Type{ast.TypeInt},
ReturnType: ast.TypeString,
Callback: func(args []interface{}) (interface{}, error) {
v, err := strconv.ParseInt(args[0].(string), 0, 0)
if err != nil {
return nil, err
}
return int(v), nil
},
}
}

View File

@ -9,9 +9,21 @@ import (
// TypeCheck implements ast.Visitor for type checking an AST tree.
// It requires some configuration to look up the type of nodes.
//
// It also optionally will not type error and will insert an implicit
// type conversions for specific types if specified by the Implicit
// field. Note that this is kind of organizationally weird to put into
// this structure but we'd rather do that than duplicate the type checking
// logic multiple times.
type TypeCheck struct {
Scope *Scope
// Implicit is a map of implicit type conversions that we can do,
// and that shouldn't error. The key of the first map is the from type,
// the key of the second map is the to type, and the final string
// value is the function to call (which must be registered in the Scope).
Implicit map[ast.Type]map[ast.Type]string
stack []ast.Type
err error
lock sync.Mutex
@ -61,6 +73,12 @@ func (v *TypeCheck) visitCall(n *ast.Call) {
// Verify the args
for i, expected := range function.ArgTypes {
if args[i] != expected {
cn := v.implicitConversion(args[i], expected, n.Args[i])
if cn != nil {
n.Args[i] = cn
continue
}
v.createErr(n, fmt.Sprintf(
"%s: argument %d should be %s, got %s",
n.Func, i+1, expected, args[i]))
@ -73,9 +91,17 @@ func (v *TypeCheck) visitCall(n *ast.Call) {
args = args[len(function.ArgTypes):]
for i, t := range args {
if t != function.VariadicType {
realI := i + len(function.ArgTypes)
cn := v.implicitConversion(
t, function.VariadicType, n.Args[realI])
if cn != nil {
n.Args[realI] = cn
continue
}
v.createErr(n, fmt.Sprintf(
"%s: argument %d should be %s, got %s",
n.Func, i+len(function.ArgTypes),
n.Func, realI,
function.VariadicType, t))
return
}
@ -95,6 +121,12 @@ func (v *TypeCheck) visitConcat(n *ast.Concat) {
// All concat args must be strings, so validate that
for i, t := range types {
if t != ast.TypeString {
cn := v.implicitConversion(t, ast.TypeString, n.Exprs[i])
if cn != nil {
n.Exprs[i] = cn
continue
}
v.createErr(n, fmt.Sprintf(
"argument %d must be a string", i+1))
return
@ -123,7 +155,32 @@ func (v *TypeCheck) visitVariableAccess(n *ast.VariableAccess) {
}
func (v *TypeCheck) createErr(n ast.Node, str string) {
v.err = fmt.Errorf("%s: %s", n.Pos(), str)
pos := n.Pos()
v.err = fmt.Errorf("At column %d, line %d: %s",
pos.Column, pos.Line, str)
}
func (v *TypeCheck) implicitConversion(
actual ast.Type, expected ast.Type, n ast.Node) ast.Node {
if v.Implicit == nil {
return nil
}
fromMap, ok := v.Implicit[actual]
if !ok {
return nil
}
toFunc, ok := fromMap[expected]
if !ok {
return nil
}
return &ast.Call{
Func: toFunc,
Args: []ast.Node{n},
Posx: n.Pos(),
}
}
func (v *TypeCheck) reset() {

View File

@ -174,3 +174,92 @@ func TestTypeCheck(t *testing.T) {
}
}
}
func TestTypeCheck_implicit(t *testing.T) {
implicitMap := map[ast.Type]map[ast.Type]string{
ast.TypeInt: {
ast.TypeString: "intToString",
},
}
cases := []struct {
Input string
Scope *Scope
Error bool
}{
{
"foo ${bar}",
&Scope{
VarMap: map[string]Variable{
"bar": Variable{
Value: 42,
Type: ast.TypeInt,
},
},
},
false,
},
{
"foo ${foo(42)}",
&Scope{
FuncMap: map[string]Function{
"foo": Function{
ArgTypes: []ast.Type{ast.TypeString},
ReturnType: ast.TypeString,
},
},
},
false,
},
{
`foo ${foo("42", 42)}`,
&Scope{
FuncMap: map[string]Function{
"foo": Function{
ArgTypes: []ast.Type{ast.TypeString},
Variadic: true,
VariadicType: ast.TypeString,
ReturnType: ast.TypeString,
},
},
},
false,
},
}
for _, tc := range cases {
node, err := Parse(tc.Input)
if err != nil {
t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input)
}
// Modify the scope to add our conversion functions.
if tc.Scope.FuncMap == nil {
tc.Scope.FuncMap = make(map[string]Function)
}
tc.Scope.FuncMap["intToString"] = Function{
ArgTypes: []ast.Type{ast.TypeInt},
ReturnType: ast.TypeString,
}
// Do the first pass...
visitor := &TypeCheck{Scope: tc.Scope, Implicit: implicitMap}
err = visitor.Visit(node)
if (err != nil) != tc.Error {
t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input)
}
if err != nil {
continue
}
// If we didn't error, then the next type check should not fail
// WITHOUT implicits.
visitor = &TypeCheck{Scope: tc.Scope}
err = visitor.Visit(node)
if err != nil {
t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input)
}
}
}

View File

@ -27,9 +27,20 @@ type SemanticChecker func(ast.Node) error
// Execute executes the given ast.Node and returns its final value, its
// type, and an error if one exists.
func (e *Engine) Execute(root ast.Node) (interface{}, ast.Type, error) {
// Copy the scope so we can add our builtins
scope := e.scope()
implicitMap := map[ast.Type]map[ast.Type]string{
ast.TypeInt: {
ast.TypeString: "__builtin_IntToString",
},
ast.TypeString: {
ast.TypeInt: "__builtin_StringToInt",
},
}
// Build our own semantic checks that we always run
tv := &TypeCheck{Scope: e.GlobalScope}
ic := &IdentifierCheck{Scope: e.GlobalScope}
tv := &TypeCheck{Scope: scope, Implicit: implicitMap}
ic := &IdentifierCheck{Scope: scope}
// Build up the semantic checks for execution
checks := make(
@ -46,10 +57,20 @@ func (e *Engine) Execute(root ast.Node) (interface{}, ast.Type, error) {
}
// Execute
v := &executeVisitor{Scope: e.GlobalScope}
v := &executeVisitor{Scope: scope}
return v.Visit(root)
}
func (e *Engine) scope() *Scope {
var scope Scope
if e.GlobalScope != nil {
scope = *e.GlobalScope
}
registerBuiltins(&scope)
return &scope
}
// executeVisitor is the visitor used to do the actual execution of
// a program. Note at this point it is assumed that the types check out
// and the identifiers exist.

View File

@ -2,6 +2,7 @@ package lang
import (
"reflect"
"strconv"
"testing"
"github.com/hashicorp/terraform/config/lang/ast"
@ -77,6 +78,41 @@ func TestEngineExecute(t *testing.T) {
"foo foobar",
ast.TypeString,
},
// Testing implicit type conversions
{
"foo ${bar}",
&Scope{
VarMap: map[string]Variable{
"bar": Variable{
Value: 42,
Type: ast.TypeInt,
},
},
},
false,
"foo 42",
ast.TypeString,
},
{
`foo ${foo("42")}`,
&Scope{
FuncMap: map[string]Function{
"foo": Function{
ArgTypes: []ast.Type{ast.TypeInt},
ReturnType: ast.TypeString,
Callback: func(args []interface{}) (interface{}, error) {
return strconv.FormatInt(int64(args[0].(int)), 10), nil
},
},
},
},
false,
"foo 42",
ast.TypeString,
},
}
for _, tc := range cases {

View File

@ -40,6 +40,23 @@ top:
| literalModeTop
{
parserResult = $1
// We want to make sure that the top value is always a Concat
// so that the return value is always a string type from an
// interpolation.
//
// The logic for checking for a LiteralNode is a little annoying
// because functionally the AST is the same, but we do that because
// it makes for an easy literal check later (to check if a string
// has any interpolations).
if _, ok := $1.(*ast.Concat); !ok {
if n, ok := $1.(*ast.LiteralNode); !ok || n.Type != ast.TypeString {
parserResult = &ast.Concat{
Exprs: []ast.Node{$1},
Posx: $1.Pos(),
}
}
}
}
literalModeTop:

View File

@ -149,23 +149,33 @@ func TestParse(t *testing.T) {
{
"${foo()}",
false,
&ast.Call{
Func: "foo",
Args: nil,
&ast.Concat{
Posx: ast.Pos{Column: 3, Line: 1},
Exprs: []ast.Node{
&ast.Call{
Func: "foo",
Args: nil,
Posx: ast.Pos{Column: 3, Line: 1},
},
},
},
},
{
"${foo(bar)}",
false,
&ast.Call{
Func: "foo",
&ast.Concat{
Posx: ast.Pos{Column: 3, Line: 1},
Args: []ast.Node{
&ast.VariableAccess{
Name: "bar",
Posx: ast.Pos{Column: 7, Line: 1},
Exprs: []ast.Node{
&ast.Call{
Func: "foo",
Posx: ast.Pos{Column: 3, Line: 1},
Args: []ast.Node{
&ast.VariableAccess{
Name: "bar",
Posx: ast.Pos{Column: 7, Line: 1},
},
},
},
},
},
@ -174,17 +184,22 @@ func TestParse(t *testing.T) {
{
"${foo(bar, baz)}",
false,
&ast.Call{
Func: "foo",
&ast.Concat{
Posx: ast.Pos{Column: 3, Line: 1},
Args: []ast.Node{
&ast.VariableAccess{
Name: "bar",
Posx: ast.Pos{Column: 7, Line: 1},
},
&ast.VariableAccess{
Name: "baz",
Posx: ast.Pos{Column: 11, Line: 1},
Exprs: []ast.Node{
&ast.Call{
Func: "foo",
Posx: ast.Pos{Column: 3, Line: 1},
Args: []ast.Node{
&ast.VariableAccess{
Name: "bar",
Posx: ast.Pos{Column: 7, Line: 1},
},
&ast.VariableAccess{
Name: "baz",
Posx: ast.Pos{Column: 11, Line: 1},
},
},
},
},
},
@ -193,17 +208,22 @@ func TestParse(t *testing.T) {
{
"${foo(bar(baz))}",
false,
&ast.Call{
Func: "foo",
&ast.Concat{
Posx: ast.Pos{Column: 3, Line: 1},
Args: []ast.Node{
Exprs: []ast.Node{
&ast.Call{
Func: "bar",
Posx: ast.Pos{Column: 7, Line: 1},
Func: "foo",
Posx: ast.Pos{Column: 3, Line: 1},
Args: []ast.Node{
&ast.VariableAccess{
Name: "baz",
Posx: ast.Pos{Column: 11, Line: 1},
&ast.Call{
Func: "bar",
Posx: ast.Pos{Column: 7, Line: 1},
Args: []ast.Node{
&ast.VariableAccess{
Name: "baz",
Posx: ast.Pos{Column: 11, Line: 1},
},
},
},
},
},
@ -251,6 +271,12 @@ func TestParse(t *testing.T) {
true,
nil,
},
{
"${var",
true,
nil,
},
}
for _, tc := range cases {

View File

@ -48,7 +48,7 @@ const parserEofCode = 1
const parserErrCode = 2
const parserMaxDepth = 200
//line lang.y:134
//line lang.y:151
//line yacctab:1
var parserExca = []int{
@ -354,14 +354,31 @@ parserdefault:
//line lang.y:41
{
parserResult = parserS[parserpt-0].node
// We want to make sure that the top value is always a Concat
// so that the return value is always a string type from an
// interpolation.
//
// The logic for checking for a LiteralNode is a little annoying
// because functionally the AST is the same, but we do that because
// it makes for an easy literal check later (to check if a string
// has any interpolations).
if _, ok := parserS[parserpt-0].node.(*ast.Concat); !ok {
if n, ok := parserS[parserpt-0].node.(*ast.LiteralNode); !ok || n.Type != ast.TypeString {
parserResult = &ast.Concat{
Exprs: []ast.Node{parserS[parserpt-0].node},
Posx: parserS[parserpt-0].node.Pos(),
}
}
}
}
case 3:
//line lang.y:47
//line lang.y:64
{
parserVAL.node = parserS[parserpt-0].node
}
case 4:
//line lang.y:51
//line lang.y:68
{
var result []ast.Node
if c, ok := parserS[parserpt-1].node.(*ast.Concat); ok {
@ -376,27 +393,27 @@ parserdefault:
}
}
case 5:
//line lang.y:67
//line lang.y:84
{
parserVAL.node = parserS[parserpt-0].node
}
case 6:
//line lang.y:71
//line lang.y:88
{
parserVAL.node = parserS[parserpt-0].node
}
case 7:
//line lang.y:77
//line lang.y:94
{
parserVAL.node = parserS[parserpt-1].node
}
case 8:
//line lang.y:83
//line lang.y:100
{
parserVAL.node = parserS[parserpt-0].node
}
case 9:
//line lang.y:87
//line lang.y:104
{
parserVAL.node = &ast.LiteralNode{
Value: parserS[parserpt-0].token.Value.(int),
@ -405,7 +422,7 @@ parserdefault:
}
}
case 10:
//line lang.y:95
//line lang.y:112
{
parserVAL.node = &ast.LiteralNode{
Value: parserS[parserpt-0].token.Value.(float64),
@ -414,32 +431,32 @@ parserdefault:
}
}
case 11:
//line lang.y:103
//line lang.y:120
{
parserVAL.node = &ast.VariableAccess{Name: parserS[parserpt-0].token.Value.(string), Posx: parserS[parserpt-0].token.Pos}
}
case 12:
//line lang.y:107
//line lang.y:124
{
parserVAL.node = &ast.Call{Func: parserS[parserpt-3].token.Value.(string), Args: parserS[parserpt-1].nodeList, Posx: parserS[parserpt-3].token.Pos}
}
case 13:
//line lang.y:112
//line lang.y:129
{
parserVAL.nodeList = nil
}
case 14:
//line lang.y:116
//line lang.y:133
{
parserVAL.nodeList = append(parserS[parserpt-2].nodeList, parserS[parserpt-0].node)
}
case 15:
//line lang.y:120
//line lang.y:137
{
parserVAL.nodeList = append(parserVAL.nodeList, parserS[parserpt-0].node)
}
case 16:
//line lang.y:126
//line lang.y:143
{
parserVAL.node = &ast.LiteralNode{
Value: parserS[parserpt-0].token.Value.(string),

View File

@ -35,25 +35,25 @@ state 2
state 3
literalModeTop: literalModeValue. (3)
. reduce 3 (src line 45)
. reduce 3 (src line 62)
state 4
literalModeValue: literal. (5)
. reduce 5 (src line 65)
. reduce 5 (src line 82)
state 5
literalModeValue: interpolation. (6)
. reduce 6 (src line 70)
. reduce 6 (src line 87)
state 6
literal: STRING. (16)
. reduce 16 (src line 124)
. reduce 16 (src line 141)
state 7
@ -75,7 +75,7 @@ state 7
state 8
literalModeTop: literalModeTop literalModeValue. (4)
. reduce 4 (src line 50)
. reduce 4 (src line 67)
state 9
@ -91,7 +91,7 @@ state 10
PROGRAM_BRACKET_LEFT shift 7
STRING shift 6
. reduce 8 (src line 81)
. reduce 8 (src line 98)
interpolation goto 5
literal goto 4
@ -100,13 +100,13 @@ state 10
state 11
expr: INTEGER. (9)
. reduce 9 (src line 86)
. reduce 9 (src line 103)
state 12
expr: FLOAT. (10)
. reduce 10 (src line 94)
. reduce 10 (src line 111)
state 13
@ -114,13 +114,13 @@ state 13
expr: IDENTIFIER.PAREN_LEFT args PAREN_RIGHT
PAREN_LEFT shift 15
. reduce 11 (src line 102)
. reduce 11 (src line 119)
state 14
interpolation: PROGRAM_BRACKET_LEFT expr PROGRAM_BRACKET_RIGHT. (7)
. reduce 7 (src line 75)
. reduce 7 (src line 92)
state 15
@ -132,7 +132,7 @@ state 15
INTEGER shift 11
FLOAT shift 12
STRING shift 6
. reduce 13 (src line 111)
. reduce 13 (src line 128)
expr goto 17
interpolation goto 5
@ -153,13 +153,13 @@ state 16
state 17
args: expr. (15)
. reduce 15 (src line 119)
. reduce 15 (src line 136)
state 18
expr: IDENTIFIER PAREN_LEFT args PAREN_RIGHT. (12)
. reduce 12 (src line 106)
. reduce 12 (src line 123)
state 19
@ -181,7 +181,7 @@ state 19
state 20
args: args COMMA expr. (14)
. reduce 14 (src line 115)
. reduce 14 (src line 132)
14 terminals, 8 nonterminals

View File

@ -80,7 +80,7 @@ func (r *RawConfig) Config() map[string]interface{} {
// Any prior calls to Interpolate are replaced with this one.
//
// If a variable key is missing, this will panic.
func (r *RawConfig) Interpolate(vs map[string]string) error {
func (r *RawConfig) Interpolate(vs map[string]lang.Variable) error {
engine := langEngine(vs)
return r.interpolate(func(root ast.Node) (string, error) {
out, _, err := engine.Execute(root)
@ -203,12 +203,7 @@ type gobRawConfig struct {
}
// langEngine returns the lang.Engine to use for evaluating configurations.
func langEngine(vs map[string]string) *lang.Engine {
varMap := make(map[string]lang.Variable)
for k, v := range vs {
varMap[k] = lang.Variable{Value: v, Type: ast.TypeString}
}
func langEngine(vs map[string]lang.Variable) *lang.Engine {
funcMap := make(map[string]lang.Function)
for k, v := range Funcs {
funcMap[k] = v
@ -217,7 +212,7 @@ func langEngine(vs map[string]string) *lang.Engine {
return &lang.Engine{
GlobalScope: &lang.Scope{
VarMap: varMap,
VarMap: vs,
FuncMap: funcMap,
},
}

View File

@ -4,6 +4,9 @@ import (
"encoding/gob"
"reflect"
"testing"
"github.com/hashicorp/terraform/config/lang"
"github.com/hashicorp/terraform/config/lang/ast"
)
func TestNewRawConfig(t *testing.T) {
@ -40,7 +43,12 @@ func TestRawConfig(t *testing.T) {
t.Fatalf("bad: %#v", rc.Config())
}
vars := map[string]string{"var.bar": "baz"}
vars := map[string]lang.Variable{
"var.bar": lang.Variable{
Value: "baz",
Type: ast.TypeString,
},
}
if err := rc.Interpolate(vars); err != nil {
t.Fatalf("err: %s", err)
}
@ -68,7 +76,12 @@ func TestRawConfig_double(t *testing.T) {
t.Fatalf("err: %s", err)
}
vars := map[string]string{"var.bar": "baz"}
vars := map[string]lang.Variable{
"var.bar": lang.Variable{
Value: "baz",
Type: ast.TypeString,
},
}
if err := rc.Interpolate(vars); err != nil {
t.Fatalf("err: %s", err)
}
@ -82,7 +95,12 @@ func TestRawConfig_double(t *testing.T) {
t.Fatalf("bad: %#v", actual)
}
vars = map[string]string{"var.bar": "what"}
vars = map[string]lang.Variable{
"var.bar": lang.Variable{
Value: "what",
Type: ast.TypeString,
},
}
if err := rc.Interpolate(vars); err != nil {
t.Fatalf("err: %s", err)
}
@ -97,6 +115,16 @@ func TestRawConfig_double(t *testing.T) {
}
}
func TestRawConfig_syntax(t *testing.T) {
raw := map[string]interface{}{
"foo": "${var",
}
if _, err := NewRawConfig(raw); err == nil {
t.Fatal("should error")
}
}
func TestRawConfig_unknown(t *testing.T) {
raw := map[string]interface{}{
"foo": "${var.bar}",
@ -107,7 +135,12 @@ func TestRawConfig_unknown(t *testing.T) {
t.Fatalf("err: %s", err)
}
vars := map[string]string{"var.bar": UnknownVariableValue}
vars := map[string]lang.Variable{
"var.bar": lang.Variable{
Value: UnknownVariableValue,
Type: ast.TypeString,
},
}
if err := rc.Interpolate(vars); err != nil {
t.Fatalf("err: %s", err)
}
@ -145,7 +178,12 @@ func TestRawConfigValue(t *testing.T) {
t.Fatalf("err: %#v", rc.Value())
}
vars := map[string]string{"var.bar": "baz"}
vars := map[string]lang.Variable{
"var.bar": lang.Variable{
Value: "baz",
Type: ast.TypeString,
},
}
if err := rc.Interpolate(vars); err != nil {
t.Fatalf("err: %s", err)
}

View File

@ -8,6 +8,8 @@ import (
"testing"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/lang"
"github.com/hashicorp/terraform/config/lang/ast"
"github.com/hashicorp/terraform/terraform"
)
@ -21,7 +23,12 @@ func testConfig(
}
if len(vs) > 0 {
if err := rc.Interpolate(vs); err != nil {
vars := make(map[string]lang.Variable)
for k, v := range vs {
vars[k] = lang.Variable{Value: v, Type: ast.TypeString}
}
if err := rc.Interpolate(vars); err != nil {
t.Fatalf("err: %s", err)
}
}

View File

@ -5,6 +5,8 @@ import (
"testing"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/lang"
"github.com/hashicorp/terraform/config/lang/ast"
"github.com/hashicorp/terraform/terraform"
)
@ -1808,7 +1810,12 @@ func TestSchemaMap_Diff(t *testing.T) {
}
if len(tc.ConfigVariables) > 0 {
if err := c.Interpolate(tc.ConfigVariables); err != nil {
vars := make(map[string]lang.Variable)
for k, v := range tc.ConfigVariables {
vars[k] = lang.Variable{Value: v, Type: ast.TypeString}
}
if err := c.Interpolate(vars); err != nil {
t.Fatalf("#%d err: %s", i, err)
}
}
@ -2580,7 +2587,12 @@ func TestSchemaMap_Validate(t *testing.T) {
t.Fatalf("err: %s", err)
}
if tc.Vars != nil {
if err := c.Interpolate(tc.Vars); err != nil {
vars := make(map[string]lang.Variable)
for k, v := range tc.Vars {
vars[k] = lang.Variable{Value: v, Type: ast.TypeString}
}
if err := c.Interpolate(vars); err != nil {
t.Fatalf("err: %s", err)
}
}

View File

@ -11,6 +11,8 @@ import (
"sync/atomic"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/lang"
"github.com/hashicorp/terraform/config/lang/ast"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/depgraph"
"github.com/hashicorp/terraform/helper/multierror"
@ -1520,9 +1522,12 @@ func (c *walkContext) computeVars(
}
// Copy the default variables
vs := make(map[string]string)
vs := make(map[string]lang.Variable)
for k, v := range c.defaultVariables {
vs[k] = v
vs[k] = lang.Variable{
Value: v,
Type: ast.TypeString,
}
}
// Next, the actual computed variables
@ -1532,12 +1537,18 @@ func (c *walkContext) computeVars(
switch v.Type {
case config.CountValueIndex:
if r != nil {
vs[n] = strconv.FormatInt(int64(r.CountIndex), 10)
vs[n] = lang.Variable{
Value: int(r.CountIndex),
Type: ast.TypeInt,
}
}
}
case *config.ModuleVariable:
if c.Operation == walkValidate {
vs[n] = config.UnknownVariableValue
vs[n] = lang.Variable{
Value: config.UnknownVariableValue,
Type: ast.TypeString,
}
continue
}
@ -1546,7 +1557,10 @@ func (c *walkContext) computeVars(
return err
}
vs[n] = value
vs[n] = lang.Variable{
Value: value,
Type: ast.TypeString,
}
case *config.PathVariable:
switch v.Type {
case config.PathValueCwd:
@ -1557,17 +1571,29 @@ func (c *walkContext) computeVars(
v.FullKey(), err)
}
vs[n] = wd
vs[n] = lang.Variable{
Value: wd,
Type: ast.TypeString,
}
case config.PathValueModule:
if t := c.Context.module.Child(c.Path[1:]); t != nil {
vs[n] = t.Config().Dir
vs[n] = lang.Variable{
Value: t.Config().Dir,
Type: ast.TypeString,
}
}
case config.PathValueRoot:
vs[n] = c.Context.module.Config().Dir
vs[n] = lang.Variable{
Value: c.Context.module.Config().Dir,
Type: ast.TypeString,
}
}
case *config.ResourceVariable:
if c.Operation == walkValidate {
vs[n] = config.UnknownVariableValue
vs[n] = lang.Variable{
Value: config.UnknownVariableValue,
Type: ast.TypeString,
}
continue
}
@ -1582,16 +1608,25 @@ func (c *walkContext) computeVars(
return err
}
vs[n] = attr
vs[n] = lang.Variable{
Value: attr,
Type: ast.TypeString,
}
case *config.UserVariable:
val, ok := c.Variables[v.Name]
if ok {
vs[n] = val
vs[n] = lang.Variable{
Value: val,
Type: ast.TypeString,
}
continue
}
if _, ok := vs[n]; !ok && c.Operation == walkValidate {
vs[n] = config.UnknownVariableValue
vs[n] = lang.Variable{
Value: config.UnknownVariableValue,
Type: ast.TypeString,
}
continue
}
@ -1599,7 +1634,10 @@ func (c *walkContext) computeVars(
// those are map overrides. Include those.
for k, val := range c.Variables {
if strings.HasPrefix(k, v.Name+".") {
vs["var."+k] = val
vs["var."+k] = lang.Variable{
Value: val,
Type: ast.TypeString,
}
}
}
}