core: Use native HIL maps instead of flatmaps

This changes the representation of maps in the interpolator from the
dotted flatmap form of a string variable named "var.variablename.key"
per map element to use native HIL maps instead.

This involves porting some of the interpolation functions in order to
keep the tests green, and adding support for map outputs.

There is one backwards incompatibility: as a result of an implementation
detail of maps, one could access an indexed map variable using the
syntax "${var.variablename.key}".

This is no longer possible - instead HIL native syntax -
"${var.variablename["key"]}" must be used. This was previously
documented, (though not heavily used) so it must be noted as a backward
compatibility issue for Terraform 0.7.
This commit is contained in:
James Nugent 2016-04-11 12:40:06 -05:00
parent 6aac79e194
commit e57a399d71
26 changed files with 438 additions and 278 deletions

View File

@ -394,29 +394,31 @@ func outputsAsString(state *terraform.State, schema []*config.Output) string {
// Output the outputs in alphabetical order // Output the outputs in alphabetical order
keyLen := 0 keyLen := 0
keys := make([]string, 0, len(outputs)) ks := make([]string, 0, len(outputs))
for key, _ := range outputs { for key, _ := range outputs {
keys = append(keys, key) ks = append(ks, key)
if len(key) > keyLen { if len(key) > keyLen {
keyLen = len(key) keyLen = len(key)
} }
} }
sort.Strings(keys) sort.Strings(ks)
for _, k := range keys {
v := outputs[k]
for _, k := range ks {
if schemaMap[k].Sensitive { if schemaMap[k].Sensitive {
outputBuf.WriteString(fmt.Sprintf( outputBuf.WriteString(fmt.Sprintf("%s = <sensitive>\n", k))
" %s%s = <sensitive>\n", continue
k, }
strings.Repeat(" ", keyLen-len(k))))
} else { v := outputs[k]
outputBuf.WriteString(fmt.Sprintf( switch typedV := v.(type) {
" %s%s = %s\n", case string:
k, outputBuf.WriteString(fmt.Sprintf("%s = %s\n", k, typedV))
strings.Repeat(" ", keyLen-len(k)), case []interface{}:
v)) outputBuf.WriteString(formatListOutput("", k, typedV))
outputBuf.WriteString("\n")
case map[string]interface{}:
outputBuf.WriteString(formatMapOutput("", k, typedV))
outputBuf.WriteString("\n")
} }
} }
} }

View File

@ -1,9 +1,11 @@
package command package command
import ( import (
"bytes"
"flag" "flag"
"fmt" "fmt"
"sort" "sort"
"strconv"
"strings" "strings"
) )
@ -27,7 +29,7 @@ func (c *OutputCommand) Run(args []string) int {
} }
args = cmdFlags.Args() args = cmdFlags.Args()
if len(args) > 1 { if len(args) > 2 {
c.Ui.Error( c.Ui.Error(
"The output command expects exactly one argument with the name\n" + "The output command expects exactly one argument with the name\n" +
"of an output variable or no arguments to show all outputs.\n") "of an output variable or no arguments to show all outputs.\n")
@ -40,6 +42,11 @@ func (c *OutputCommand) Run(args []string) int {
name = args[0] name = args[0]
} }
index := ""
if len(args) > 1 {
index = args[1]
}
stateStore, err := c.Meta.State() stateStore, err := c.Meta.State()
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf("Error reading state: %s", err)) c.Ui.Error(fmt.Sprintf("Error reading state: %s", err))
@ -74,17 +81,7 @@ func (c *OutputCommand) Run(args []string) int {
} }
if name == "" { if name == "" {
ks := make([]string, 0, len(mod.Outputs)) c.Ui.Output(outputsAsString(state))
for k, _ := range mod.Outputs {
ks = append(ks, k)
}
sort.Strings(ks)
for _, k := range ks {
v := mod.Outputs[k]
c.Ui.Output(fmt.Sprintf("%s = %s", k, v))
}
return 0 return 0
} }
@ -101,6 +98,44 @@ func (c *OutputCommand) Run(args []string) int {
switch output := v.(type) { switch output := v.(type) {
case string: case string:
c.Ui.Output(output) c.Ui.Output(output)
return 0
case []interface{}:
if index == "" {
c.Ui.Output(formatListOutput("", "", output))
break
}
indexInt, err := strconv.Atoi(index)
if err != nil {
c.Ui.Error(fmt.Sprintf(
"The index %q requested is not valid for the list output\n"+
"%q - indices must be numeric, and in the range 0-%d", index, name,
len(output)-1))
break
}
if indexInt < 0 || indexInt >= len(output) {
c.Ui.Error(fmt.Sprintf(
"The index %d requested is not valid for the list output\n"+
"%q - indices must be in the range 0-%d", indexInt, name,
len(output)-1))
break
}
c.Ui.Output(fmt.Sprintf("%s", output[indexInt]))
return 0
case map[string]interface{}:
if index == "" {
c.Ui.Output(formatMapOutput("", "", output))
break
}
if value, ok := output[index]; ok {
c.Ui.Output(fmt.Sprintf("%s", value))
return 0
} else {
return 1
}
default: default:
panic(fmt.Errorf("Unknown output type: %T", output)) panic(fmt.Errorf("Unknown output type: %T", output))
} }
@ -108,6 +143,53 @@ func (c *OutputCommand) Run(args []string) int {
return 0 return 0
} }
func formatListOutput(indent, outputName string, outputList []interface{}) string {
keyIndent := ""
outputBuf := new(bytes.Buffer)
if outputName != "" {
outputBuf.WriteString(fmt.Sprintf("%s%s = [", indent, outputName))
keyIndent = " "
}
for _, value := range outputList {
outputBuf.WriteString(fmt.Sprintf("\n%s%s%s", indent, keyIndent, value))
}
if outputName != "" {
outputBuf.WriteString(fmt.Sprintf("\n%s]", indent))
}
return strings.TrimPrefix(outputBuf.String(), "\n")
}
func formatMapOutput(indent, outputName string, outputMap map[string]interface{}) string {
ks := make([]string, 0, len(outputMap))
for k, _ := range outputMap {
ks = append(ks, k)
}
sort.Strings(ks)
keyIndent := ""
outputBuf := new(bytes.Buffer)
if outputName != "" {
outputBuf.WriteString(fmt.Sprintf("%s%s = {", indent, outputName))
keyIndent = " "
}
for _, k := range ks {
v := outputMap[k]
outputBuf.WriteString(fmt.Sprintf("\n%s%s%s = %v", indent, keyIndent, k, v))
}
if outputName != "" {
outputBuf.WriteString(fmt.Sprintf("\n%s}", indent))
}
return strings.TrimPrefix(outputBuf.String(), "\n")
}
func (c *OutputCommand) Help() string { func (c *OutputCommand) Help() string {
helpText := ` helpText := `
Usage: terraform output [options] [NAME] Usage: terraform output [options] [NAME]

View File

@ -11,7 +11,6 @@ import (
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
"github.com/hashicorp/hil" "github.com/hashicorp/hil"
"github.com/hashicorp/hil/ast" "github.com/hashicorp/hil/ast"
"github.com/hashicorp/terraform/flatmap"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"github.com/mitchellh/reflectwalk" "github.com/mitchellh/reflectwalk"
) )
@ -239,7 +238,7 @@ func (c *Config) Validate() error {
} }
interp := false interp := false
fn := func(ast.Node) (string, error) { fn := func(ast.Node) (interface{}, error) {
interp = true interp = true
return "", nil return "", nil
} }
@ -450,7 +449,7 @@ func (c *Config) Validate() error {
} }
// Interpolate with a fixed number to verify that its a number. // Interpolate with a fixed number to verify that its a number.
r.RawCount.interpolate(func(root ast.Node) (string, error) { r.RawCount.interpolate(func(root ast.Node) (interface{}, error) {
// Execute the node but transform the AST so that it returns // Execute the node but transform the AST so that it returns
// a fixed value of "5" for all interpolations. // a fixed value of "5" for all interpolations.
result, err := hil.Eval( result, err := hil.Eval(
@ -461,7 +460,7 @@ func (c *Config) Validate() error {
return "", err return "", err
} }
return result.Value.(string), nil return result.Value, nil
}) })
_, err := strconv.ParseInt(r.RawCount.Value().(string), 0, 0) _, err := strconv.ParseInt(r.RawCount.Value().(string), 0, 0)
if err != nil { if err != nil {
@ -809,28 +808,6 @@ func (r *Resource) mergerMerge(m merger) merger {
return &result return &result
} }
// DefaultsMap returns a map of default values for this variable.
func (v *Variable) DefaultsMap() map[string]string {
if v.Default == nil {
return nil
}
n := fmt.Sprintf("var.%s", v.Name)
switch v.Type() {
case VariableTypeString:
return map[string]string{n: v.Default.(string)}
case VariableTypeMap:
result := flatmap.Flatten(map[string]interface{}{
n: v.Default.(map[string]string),
})
result[n] = v.Name
return result
default:
return nil
}
}
// Merge merges two variables to create a new third variable. // Merge merges two variables to create a new third variable.
func (v *Variable) Merge(v2 *Variable) *Variable { func (v *Variable) Merge(v2 *Variable) *Variable {
// Shallow copy the variable // Shallow copy the variable

View File

@ -2,7 +2,6 @@ package config
import ( import (
"path/filepath" "path/filepath"
"reflect"
"strings" "strings"
"testing" "testing"
) )
@ -458,43 +457,6 @@ func TestProviderConfigName(t *testing.T) {
} }
} }
func TestVariableDefaultsMap(t *testing.T) {
cases := []struct {
Default interface{}
Output map[string]string
}{
{
nil,
nil,
},
{
"foo",
map[string]string{"var.foo": "foo"},
},
{
map[interface{}]interface{}{
"foo": "bar",
"bar": "baz",
},
map[string]string{
"var.foo": "foo",
"var.foo.foo": "bar",
"var.foo.bar": "baz",
},
},
}
for i, tc := range cases {
v := &Variable{Name: "foo", Default: tc.Default}
actual := v.DefaultsMap()
if !reflect.DeepEqual(actual, tc.Output) {
t.Fatalf("%d: bad: %#v", i, actual)
}
}
}
func testConfig(t *testing.T, name string) *Config { func testConfig(t *testing.T, name string) *Config {
c, err := LoadFile(filepath.Join(fixtureDir, name, "main.tf")) c, err := LoadFile(filepath.Join(fixtureDir, name, "main.tf"))
if err != nil { if err != nil {

View File

@ -19,6 +19,7 @@ import (
"github.com/apparentlymart/go-cidr/cidr" "github.com/apparentlymart/go-cidr/cidr"
"github.com/hashicorp/go-uuid" "github.com/hashicorp/go-uuid"
"github.com/hashicorp/hil"
"github.com/hashicorp/hil/ast" "github.com/hashicorp/hil/ast"
"github.com/mitchellh/go-homedir" "github.com/mitchellh/go-homedir"
) )
@ -466,20 +467,22 @@ func interpolationFuncSplit() ast.Function {
// dynamic lookups of map types within a Terraform configuration. // dynamic lookups of map types within a Terraform configuration.
func interpolationFuncLookup(vs map[string]ast.Variable) ast.Function { func interpolationFuncLookup(vs map[string]ast.Variable) ast.Function {
return ast.Function{ return ast.Function{
ArgTypes: []ast.Type{ast.TypeString, ast.TypeString}, ArgTypes: []ast.Type{ast.TypeMap, ast.TypeString},
ReturnType: ast.TypeString, ReturnType: ast.TypeString,
Callback: func(args []interface{}) (interface{}, error) { Callback: func(args []interface{}) (interface{}, error) {
k := fmt.Sprintf("var.%s.%s", args[0].(string), args[1].(string)) index := args[1].(string)
v, ok := vs[k] mapVar := args[0].(map[string]ast.Variable)
v, ok := mapVar[index]
if !ok { if !ok {
return "", fmt.Errorf( return "", fmt.Errorf(
"lookup in '%s' failed to find '%s'", "lookup failed to find '%s'",
args[0].(string), args[1].(string)) args[1].(string))
} }
if v.Type != ast.TypeString { if v.Type != ast.TypeString {
return "", fmt.Errorf( return "", fmt.Errorf(
"lookup in '%s' for '%s' has bad type %s", "lookup for '%s' has bad type %s",
args[0].(string), args[1].(string), v.Type) args[1].(string), v.Type)
} }
return v.Value.(string), nil return v.Value.(string), nil
@ -513,28 +516,24 @@ func interpolationFuncElement() ast.Function {
// keys of map types within a Terraform configuration. // keys of map types within a Terraform configuration.
func interpolationFuncKeys(vs map[string]ast.Variable) ast.Function { func interpolationFuncKeys(vs map[string]ast.Variable) ast.Function {
return ast.Function{ return ast.Function{
ArgTypes: []ast.Type{ast.TypeString}, ArgTypes: []ast.Type{ast.TypeMap},
ReturnType: ast.TypeString, ReturnType: ast.TypeList,
Callback: func(args []interface{}) (interface{}, error) { Callback: func(args []interface{}) (interface{}, error) {
// Prefix must include ending dot to be a map mapVar := args[0].(map[string]ast.Variable)
prefix := fmt.Sprintf("var.%s.", args[0].(string)) keys := make([]string, 0)
keys := make([]string, 0, len(vs))
for k, _ := range vs {
if !strings.HasPrefix(k, prefix) {
continue
}
keys = append(keys, k[len(prefix):])
}
if len(keys) <= 0 { for k, _ := range mapVar {
return "", fmt.Errorf( keys = append(keys, k)
"failed to find map '%s'",
args[0].(string))
} }
sort.Strings(keys) sort.Strings(keys)
return NewStringList(keys).String(), nil variable, err := hil.InterfaceToVariable(keys)
if err != nil {
return nil, err
}
return variable.Value, nil
}, },
} }
} }
@ -543,38 +542,34 @@ func interpolationFuncKeys(vs map[string]ast.Variable) ast.Function {
// keys of map types within a Terraform configuration. // keys of map types within a Terraform configuration.
func interpolationFuncValues(vs map[string]ast.Variable) ast.Function { func interpolationFuncValues(vs map[string]ast.Variable) ast.Function {
return ast.Function{ return ast.Function{
ArgTypes: []ast.Type{ast.TypeString}, ArgTypes: []ast.Type{ast.TypeMap},
ReturnType: ast.TypeString, ReturnType: ast.TypeList,
Callback: func(args []interface{}) (interface{}, error) { Callback: func(args []interface{}) (interface{}, error) {
// Prefix must include ending dot to be a map mapVar := args[0].(map[string]ast.Variable)
prefix := fmt.Sprintf("var.%s.", args[0].(string)) keys := make([]string, 0)
keys := make([]string, 0, len(vs))
for k, _ := range vs {
if !strings.HasPrefix(k, prefix) {
continue
}
keys = append(keys, k)
}
if len(keys) <= 0 { for k, _ := range mapVar {
return "", fmt.Errorf( keys = append(keys, k)
"failed to find map '%s'",
args[0].(string))
} }
sort.Strings(keys) sort.Strings(keys)
vals := make([]string, 0, len(keys)) values := make([]string, len(keys))
for index, key := range keys {
for _, k := range keys { if value, ok := mapVar[key].Value.(string); ok {
v := vs[k] values[index] = value
if v.Type != ast.TypeString { } else {
return "", fmt.Errorf("values(): %q has bad type %s", k, v.Type) return "", fmt.Errorf("values(): %q has element with bad type %s",
key, mapVar[key].Type)
} }
vals = append(vals, vs[k].Value.(string))
} }
return NewStringList(vals).String(), nil variable, err := hil.InterfaceToVariable(values)
if err != nil {
return nil, err
}
return variable.Value, nil
}, },
} }
} }

View File

@ -672,28 +672,33 @@ func TestInterpolateFuncSplit(t *testing.T) {
func TestInterpolateFuncLookup(t *testing.T) { func TestInterpolateFuncLookup(t *testing.T) {
testFunction(t, testFunctionConfig{ testFunction(t, testFunctionConfig{
Vars: map[string]ast.Variable{ Vars: map[string]ast.Variable{
"var.foo.bar": ast.Variable{ "var.foo": ast.Variable{
Value: "baz", Type: ast.TypeMap,
Type: ast.TypeString, Value: map[string]ast.Variable{
"bar": ast.Variable{
Type: ast.TypeString,
Value: "baz",
},
},
}, },
}, },
Cases: []testFunctionCase{ Cases: []testFunctionCase{
{ {
`${lookup("foo", "bar")}`, `${lookup(var.foo, "bar")}`,
"baz", "baz",
false, false,
}, },
// Invalid key // Invalid key
{ {
`${lookup("foo", "baz")}`, `${lookup(var.foo, "baz")}`,
nil, nil,
true, true,
}, },
// Too many args // Too many args
{ {
`${lookup("foo", "bar", "baz")}`, `${lookup(var.foo, "bar", "baz")}`,
nil, nil,
true, true,
}, },
@ -704,13 +709,18 @@ func TestInterpolateFuncLookup(t *testing.T) {
func TestInterpolateFuncKeys(t *testing.T) { func TestInterpolateFuncKeys(t *testing.T) {
testFunction(t, testFunctionConfig{ testFunction(t, testFunctionConfig{
Vars: map[string]ast.Variable{ Vars: map[string]ast.Variable{
"var.foo.bar": ast.Variable{ "var.foo": ast.Variable{
Value: "baz", Type: ast.TypeMap,
Type: ast.TypeString, Value: map[string]ast.Variable{
}, "bar": ast.Variable{
"var.foo.qux": ast.Variable{ Value: "baz",
Value: "quack", Type: ast.TypeString,
Type: ast.TypeString, },
"qux": ast.Variable{
Value: "quack",
Type: ast.TypeString,
},
},
}, },
"var.str": ast.Variable{ "var.str": ast.Variable{
Value: "astring", Value: "astring",
@ -719,28 +729,28 @@ func TestInterpolateFuncKeys(t *testing.T) {
}, },
Cases: []testFunctionCase{ Cases: []testFunctionCase{
{ {
`${keys("foo")}`, `${keys(var.foo)}`,
NewStringList([]string{"bar", "qux"}).String(), []interface{}{"bar", "qux"},
false, false,
}, },
// Invalid key // Invalid key
{ {
`${keys("not")}`, `${keys(var.not)}`,
nil, nil,
true, true,
}, },
// Too many args // Too many args
{ {
`${keys("foo", "bar")}`, `${keys(var.foo, "bar")}`,
nil, nil,
true, true,
}, },
// Not a map // Not a map
{ {
`${keys("str")}`, `${keys(var.str)}`,
nil, nil,
true, true,
}, },
@ -751,13 +761,18 @@ func TestInterpolateFuncKeys(t *testing.T) {
func TestInterpolateFuncValues(t *testing.T) { func TestInterpolateFuncValues(t *testing.T) {
testFunction(t, testFunctionConfig{ testFunction(t, testFunctionConfig{
Vars: map[string]ast.Variable{ Vars: map[string]ast.Variable{
"var.foo.bar": ast.Variable{ "var.foo": ast.Variable{
Value: "quack", Type: ast.TypeMap,
Type: ast.TypeString, Value: map[string]ast.Variable{
}, "bar": ast.Variable{
"var.foo.qux": ast.Variable{ Value: "quack",
Value: "baz", Type: ast.TypeString,
Type: ast.TypeString, },
"qux": ast.Variable{
Value: "baz",
Type: ast.TypeString,
},
},
}, },
"var.str": ast.Variable{ "var.str": ast.Variable{
Value: "astring", Value: "astring",
@ -766,28 +781,28 @@ func TestInterpolateFuncValues(t *testing.T) {
}, },
Cases: []testFunctionCase{ Cases: []testFunctionCase{
{ {
`${values("foo")}`, `${values(var.foo)}`,
NewStringList([]string{"quack", "baz"}).String(), []interface{}{"quack", "baz"},
false, false,
}, },
// Invalid key // Invalid key
{ {
`${values("not")}`, `${values(var.not)}`,
nil, nil,
true, true,
}, },
// Too many args // Too many args
{ {
`${values("foo", "bar")}`, `${values(var.foo, "bar")}`,
nil, nil,
true, true,
}, },
// Not a map // Not a map
{ {
`${values("str")}`, `${values(var.str)}`,
nil, nil,
true, true,
}, },

View File

@ -42,7 +42,7 @@ type interpolationWalker struct {
// //
// If Replace is set to false in interpolationWalker, then the replace // If Replace is set to false in interpolationWalker, then the replace
// value can be anything as it will have no effect. // value can be anything as it will have no effect.
type interpolationWalkerFunc func(ast.Node) (string, error) type interpolationWalkerFunc func(ast.Node) (interface{}, error)
// interpolationWalkerContextFunc is called by interpolationWalk if // interpolationWalkerContextFunc is called by interpolationWalk if
// ContextF is set. This receives both the interpolation and the location // ContextF is set. This receives both the interpolation and the location
@ -150,8 +150,8 @@ 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) { if w.loc == reflectwalk.SliceElem && IsStringList(replaceVal.(string)) {
parts := StringList(replaceVal).Slice() parts := StringList(replaceVal.(string)).Slice()
for _, p := range parts { for _, p := range parts {
if p == UnknownVariableValue { if p == UnknownVariableValue {
remove = true remove = true

View File

@ -89,7 +89,7 @@ func TestInterpolationWalker_detect(t *testing.T) {
for i, tc := range cases { for i, tc := range cases {
var actual []string var actual []string
detectFn := func(root ast.Node) (string, error) { detectFn := func(root ast.Node) (interface{}, error) {
actual = append(actual, fmt.Sprintf("%s", root)) actual = append(actual, fmt.Sprintf("%s", root))
return "", nil return "", nil
} }
@ -175,7 +175,7 @@ func TestInterpolationWalker_replace(t *testing.T) {
} }
for i, tc := range cases { for i, tc := range cases {
fn := func(ast.Node) (string, error) { fn := func(ast.Node) (interface{}, error) {
return tc.Value, nil return tc.Value, nil
} }

View File

@ -108,7 +108,7 @@ func (r *RawConfig) Interpolate(vs map[string]ast.Variable) error {
defer r.lock.Unlock() defer r.lock.Unlock()
config := langEvalConfig(vs) config := langEvalConfig(vs)
return r.interpolate(func(root ast.Node) (string, error) { return r.interpolate(func(root ast.Node) (interface{}, error) {
// We detect the variables again and check if the value of any // We detect the variables again and check if the value of any
// of the variables is the computed value. If it is, then we // of the variables is the computed value. If it is, then we
// treat this entire value as computed. // treat this entire value as computed.
@ -137,7 +137,7 @@ func (r *RawConfig) Interpolate(vs map[string]ast.Variable) error {
return "", err return "", err
} }
return result.Value.(string), nil return result.Value, nil
}) })
} }
@ -194,7 +194,7 @@ func (r *RawConfig) init() error {
r.Interpolations = nil r.Interpolations = nil
r.Variables = nil r.Variables = nil
fn := func(node ast.Node) (string, error) { fn := func(node ast.Node) (interface{}, error) {
r.Interpolations = append(r.Interpolations, node) r.Interpolations = append(r.Interpolations, node)
vars, err := DetectVariables(node) vars, err := DetectVariables(node)
if err != nil { if err != nil {

View File

@ -48,6 +48,45 @@ func TestContext2Apply(t *testing.T) {
} }
} }
func TestContext2Apply_mapVarBetweenModules(t *testing.T) {
m := testModule(t, "apply-map-var-through-module")
p := testProvider("null")
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"null": testProviderFuncFixed(p),
},
})
if _, err := ctx.Plan(); err != nil {
t.Fatalf("err: %s", err)
}
state, err := ctx.Apply()
if err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(`<no state>
Outputs:
amis_from_module = {eu-west-1:ami-789012 eu-west-2:ami-989484 us-west-1:ami-123456 us-west-2:ami-456789 }
module.test:
null_resource.noop:
ID = foo
Outputs:
amis_out = {eu-west-1:ami-789012 eu-west-2:ami-989484 us-west-1:ami-123456 us-west-2:ami-456789 }`)
if actual != expected {
t.Fatalf("expected: \n%s\n\ngot: \n%s\n", expected, actual)
}
}
func TestContext2Apply_providerAlias(t *testing.T) { func TestContext2Apply_providerAlias(t *testing.T) {
m := testModule(t, "apply-provider-alias") m := testModule(t, "apply-provider-alias")
p := testProvider("aws") p := testProvider("aws")
@ -3066,7 +3105,7 @@ func TestContext2Apply_outputInvalid(t *testing.T) {
if err == nil { if err == nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
if !strings.Contains(err.Error(), "is not a string") { if !strings.Contains(err.Error(), "is not a valid type") {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
} }
@ -3144,7 +3183,7 @@ func TestContext2Apply_outputList(t *testing.T) {
actual := strings.TrimSpace(state.String()) actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyOutputListStr) expected := strings.TrimSpace(testTerraformApplyOutputListStr)
if actual != expected { if actual != expected {
t.Fatalf("bad: \n%s", actual) t.Fatalf("expected: \n%s\n\nbad: \n%s", expected, actual)
} }
} }
@ -3850,7 +3889,7 @@ func TestContext2Apply_vars(t *testing.T) {
actual := strings.TrimSpace(state.String()) actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyVarsStr) expected := strings.TrimSpace(testTerraformApplyVarsStr)
if actual != expected { if actual != expected {
t.Fatalf("bad: \n%s", actual) t.Fatalf("expected: %s\n got:\n%s", expected, actual)
} }
} }

View File

@ -45,7 +45,7 @@ func TestContext2Input(t *testing.T) {
actual := strings.TrimSpace(state.String()) actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformInputVarsStr) expected := strings.TrimSpace(testTerraformInputVarsStr)
if actual != expected { if actual != expected {
t.Fatalf("bad: \n%s", actual) t.Fatalf("expected:\n%s\ngot:\n%s", expected, actual)
} }
} }

View File

@ -68,7 +68,7 @@ type EvalContext interface {
// SetVariables sets the variables for the module within // SetVariables sets the variables for the module within
// this context with the name n. This function call is additive: // this context with the name n. This function call is additive:
// the second parameter is merged with any previous call. // the second parameter is merged with any previous call.
SetVariables(string, map[string]string) SetVariables(string, map[string]interface{})
// Diff returns the global diff as well as the lock that should // Diff returns the global diff as well as the lock that should
// be used to modify that diff. // be used to modify that diff.

View File

@ -23,7 +23,7 @@ type BuiltinEvalContext struct {
// as the Interpolater itself, it is protected by InterpolaterVarLock // as the Interpolater itself, it is protected by InterpolaterVarLock
// which must be locked during any access to the map. // which must be locked during any access to the map.
Interpolater *Interpolater Interpolater *Interpolater
InterpolaterVars map[string]map[string]string InterpolaterVars map[string]map[string]interface{}
InterpolaterVarLock *sync.Mutex InterpolaterVarLock *sync.Mutex
Hooks []Hook Hooks []Hook
@ -311,7 +311,7 @@ func (ctx *BuiltinEvalContext) Path() []string {
return ctx.PathValue return ctx.PathValue
} }
func (ctx *BuiltinEvalContext) SetVariables(n string, vs map[string]string) { func (ctx *BuiltinEvalContext) SetVariables(n string, vs map[string]interface{}) {
ctx.InterpolaterVarLock.Lock() ctx.InterpolaterVarLock.Lock()
defer ctx.InterpolaterVarLock.Unlock() defer ctx.InterpolaterVarLock.Unlock()
@ -322,7 +322,7 @@ func (ctx *BuiltinEvalContext) SetVariables(n string, vs map[string]string) {
vars := ctx.InterpolaterVars[key] vars := ctx.InterpolaterVars[key]
if vars == nil { if vars == nil {
vars = make(map[string]string) vars = make(map[string]interface{})
ctx.InterpolaterVars[key] = vars ctx.InterpolaterVars[key] = vars
} }

View File

@ -74,7 +74,7 @@ type MockEvalContext struct {
SetVariablesCalled bool SetVariablesCalled bool
SetVariablesModule string SetVariablesModule string
SetVariablesVariables map[string]string SetVariablesVariables map[string]interface{}
DiffCalled bool DiffCalled bool
DiffDiff *Diff DiffDiff *Diff
@ -183,7 +183,7 @@ func (c *MockEvalContext) Path() []string {
return c.PathPath return c.PathPath
} }
func (c *MockEvalContext) SetVariables(n string, vs map[string]string) { func (c *MockEvalContext) SetVariables(n string, vs map[string]interface{}) {
c.SetVariablesCalled = true c.SetVariablesCalled = true
c.SetVariablesModule = n c.SetVariablesModule = n
c.SetVariablesVariables = vs c.SetVariablesVariables = vs

View File

@ -2,6 +2,7 @@ package terraform
import ( import (
"fmt" "fmt"
"log"
"github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config"
) )
@ -45,7 +46,8 @@ type EvalWriteOutput struct {
func (n *EvalWriteOutput) Eval(ctx EvalContext) (interface{}, error) { func (n *EvalWriteOutput) Eval(ctx EvalContext) (interface{}, error) {
cfg, err := ctx.Interpolate(n.Value, nil) cfg, err := ctx.Interpolate(n.Value, nil)
if err != nil { if err != nil {
// Ignore it // Log error but continue anyway
log.Printf("[WARN] Output interpolation %q failed: %s", n.Name, err)
} }
state, lock := ctx.State() state, lock := ctx.State()
@ -76,16 +78,16 @@ func (n *EvalWriteOutput) Eval(ctx EvalContext) (interface{}, error) {
} }
} }
// If it is a list of values, get the first one switch valueTyped := valueRaw.(type) {
if list, ok := valueRaw.([]interface{}); ok { case string:
valueRaw = list[0] mod.Outputs[n.Name] = valueTyped
case []interface{}:
mod.Outputs[n.Name] = valueTyped
case map[string]interface{}:
mod.Outputs[n.Name] = valueTyped
default:
return nil, fmt.Errorf("output %s is not a valid type (%T)\n", n.Name, valueTyped)
} }
if _, ok := valueRaw.(string); !ok {
return nil, fmt.Errorf("output %s is not a string", n.Name)
}
// Write the output
mod.Outputs[n.Name] = valueRaw.(string)
return nil, nil return nil, nil
} }

View File

@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/config/module"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
@ -26,7 +25,7 @@ import (
// use of the values since it is only valid to pass string values. The // use of the values since it is only valid to pass string values. The
// structure is in place for extension of the type system, however. // structure is in place for extension of the type system, however.
type EvalTypeCheckVariable struct { type EvalTypeCheckVariable struct {
Variables map[string]string Variables map[string]interface{}
ModulePath []string ModulePath []string
ModuleTree *module.Tree ModuleTree *module.Tree
} }
@ -43,12 +42,18 @@ func (n *EvalTypeCheckVariable) Eval(ctx EvalContext) (interface{}, error) {
prototypes[variable.Name] = variable.Type() prototypes[variable.Name] = variable.Type()
} }
// Only display a module in an error message if we are not in the root module
modulePathDescription := fmt.Sprintf(" in module %s", strings.Join(n.ModulePath[1:], "."))
if len(n.ModulePath) == 1 {
modulePathDescription = ""
}
for name, declaredType := range prototypes { for name, declaredType := range prototypes {
// This is only necessary when we _actually_ check. It is left as a reminder // This is only necessary when we _actually_ check. It is left as a reminder
// that at the current time we are dealing with a type system consisting only // that at the current time we are dealing with a type system consisting only
// of strings and maps - where the only valid inter-module variable type is // of strings and maps - where the only valid inter-module variable type is
// string. // string.
_, ok := n.Variables[name] proposedValue, ok := n.Variables[name]
if !ok { if !ok {
// This means the default value should be used as no overriding value // This means the default value should be used as no overriding value
// has been set. Therefore we should continue as no check is necessary. // has been set. Therefore we should continue as no check is necessary.
@ -59,13 +64,23 @@ func (n *EvalTypeCheckVariable) Eval(ctx EvalContext) (interface{}, error) {
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
// a map[string]string but this is sufficient for now. // a map[string]string but this is sufficient for now.
continue switch proposedValue.(type) {
default: case string:
// Only display a module if we are not in the root module continue
modulePathDescription := fmt.Sprintf(" in module %s", strings.Join(n.ModulePath[1:], ".")) default:
if len(n.ModulePath) == 1 { return nil, fmt.Errorf("variable %s%s should be type %s, got %T",
modulePathDescription = "" name, modulePathDescription, declaredType.Printable(), proposedValue)
} }
continue
case config.VariableTypeMap:
switch proposedValue.(type) {
case map[string]interface{}:
continue
default:
return nil, fmt.Errorf("variable %s%s should be type %s, got %T",
name, modulePathDescription, declaredType.Printable(), proposedValue)
}
default:
// This will need the actual type substituting when we have more than // This will need the actual type substituting when we have more than
// just strings and maps. // just strings and maps.
return nil, fmt.Errorf("variable %s%s should be type %s, got type string", return nil, fmt.Errorf("variable %s%s should be type %s, got type string",
@ -80,7 +95,7 @@ func (n *EvalTypeCheckVariable) Eval(ctx EvalContext) (interface{}, error) {
// explicitly for interpolation later. // explicitly for interpolation later.
type EvalSetVariables struct { type EvalSetVariables struct {
Module *string Module *string
Variables map[string]string Variables map[string]interface{}
} }
// TODO: test // TODO: test
@ -93,31 +108,43 @@ func (n *EvalSetVariables) Eval(ctx EvalContext) (interface{}, error) {
// given configuration, and uses the final values as a way to set the // given configuration, and uses the final values as a way to set the
// mapping. // mapping.
type EvalVariableBlock struct { type EvalVariableBlock struct {
Config **ResourceConfig Config **ResourceConfig
Variables map[string]string VariableValues map[string]interface{}
} }
// TODO: test // TODO: test
func (n *EvalVariableBlock) Eval(ctx EvalContext) (interface{}, error) { func (n *EvalVariableBlock) Eval(ctx EvalContext) (interface{}, error) {
// Clear out the existing mapping // Clear out the existing mapping
for k, _ := range n.Variables { for k, _ := range n.VariableValues {
delete(n.Variables, k) delete(n.VariableValues, k)
} }
// Get our configuration // Get our configuration
rc := *n.Config rc := *n.Config
for k, v := range rc.Config { for k, v := range rc.Config {
var vStr string var vString string
if err := mapstructure.WeakDecode(v, &vStr); err != nil { if err := mapstructure.WeakDecode(v, &vString); err == nil {
return nil, errwrap.Wrapf(fmt.Sprintf( n.VariableValues[k] = vString
"%s: error reading value: {{err}}", k), err) continue
} }
n.Variables[k] = vStr var vMap map[string]interface{}
if err := mapstructure.WeakDecode(v, &vMap); err == nil {
n.VariableValues[k] = vMap
continue
}
var vSlice []interface{}
if err := mapstructure.WeakDecode(v, &vSlice); err == nil {
n.VariableValues[k] = vSlice
continue
}
return nil, fmt.Errorf("Variable value for %s is not a string, list or map type", k)
} }
for k, _ := range rc.Raw { for k, _ := range rc.Raw {
if _, ok := n.Variables[k]; !ok { if _, ok := n.VariableValues[k]; !ok {
n.Variables[k] = config.UnknownVariableValue n.VariableValues[k] = config.UnknownVariableValue
} }
} }

View File

@ -69,7 +69,7 @@ func (n *GraphNodeConfigModule) Expand(b GraphBuilder) (GraphNodeSubgraph, error
return &graphNodeModuleExpanded{ return &graphNodeModuleExpanded{
Original: n, Original: n,
Graph: graph, Graph: graph,
Variables: make(map[string]string), Variables: make(map[string]interface{}),
}, nil }, nil
} }
@ -107,7 +107,7 @@ type graphNodeModuleExpanded struct {
// Variables is a map of the input variables. This reference should // Variables is a map of the input variables. This reference should
// be shared with ModuleInputTransformer in order to create a connection // be shared with ModuleInputTransformer in order to create a connection
// where the variables are set properly. // where the variables are set properly.
Variables map[string]string Variables map[string]interface{}
} }
func (n *graphNodeModuleExpanded) Name() string { func (n *graphNodeModuleExpanded) Name() string {
@ -147,8 +147,8 @@ func (n *graphNodeModuleExpanded) EvalTree() EvalNode {
}, },
&EvalVariableBlock{ &EvalVariableBlock{
Config: &resourceConfig, Config: &resourceConfig,
Variables: n.Variables, VariableValues: n.Variables,
}, },
}, },
} }

View File

@ -114,7 +114,7 @@ func (n *GraphNodeConfigVariable) EvalTree() EvalNode {
// Otherwise, interpolate the value of this variable and set it // Otherwise, interpolate the value of this variable and set it
// within the variables mapping. // within the variables mapping.
var config *ResourceConfig var config *ResourceConfig
variables := make(map[string]string) variables := make(map[string]interface{})
return &EvalSequence{ return &EvalSequence{
Nodes: []EvalNode{ Nodes: []EvalNode{
&EvalInterpolate{ &EvalInterpolate{
@ -123,8 +123,8 @@ func (n *GraphNodeConfigVariable) EvalTree() EvalNode {
}, },
&EvalVariableBlock{ &EvalVariableBlock{
Config: &config, Config: &config,
Variables: variables, VariableValues: variables,
}, },
&EvalTypeCheckVariable{ &EvalTypeCheckVariable{

View File

@ -27,7 +27,7 @@ type ContextGraphWalker struct {
once sync.Once once sync.Once
contexts map[string]*BuiltinEvalContext contexts map[string]*BuiltinEvalContext
contextLock sync.Mutex contextLock sync.Mutex
interpolaterVars map[string]map[string]string interpolaterVars map[string]map[string]interface{}
interpolaterVarLock sync.Mutex interpolaterVarLock sync.Mutex
providerCache map[string]ResourceProvider providerCache map[string]ResourceProvider
providerConfigCache map[string]*ResourceConfig providerConfigCache map[string]*ResourceConfig
@ -49,7 +49,7 @@ func (w *ContextGraphWalker) EnterPath(path []string) EvalContext {
} }
// Setup the variables for this interpolater // Setup the variables for this interpolater
variables := make(map[string]string) variables := make(map[string]interface{})
if len(path) <= 1 { if len(path) <= 1 {
for k, v := range w.Context.variables { for k, v := range w.Context.variables {
variables[k] = v variables[k] = v
@ -81,12 +81,12 @@ func (w *ContextGraphWalker) EnterPath(path []string) EvalContext {
StateValue: w.Context.state, StateValue: w.Context.state,
StateLock: &w.Context.stateLock, StateLock: &w.Context.stateLock,
Interpolater: &Interpolater{ Interpolater: &Interpolater{
Operation: w.Operation, Operation: w.Operation,
Module: w.Context.module, Module: w.Context.module,
State: w.Context.state, State: w.Context.state,
StateLock: &w.Context.stateLock, StateLock: &w.Context.stateLock,
Variables: variables, VariableValues: variables,
VariablesLock: &w.interpolaterVarLock, VariableValuesLock: &w.interpolaterVarLock,
}, },
InterpolaterVars: w.interpolaterVars, InterpolaterVars: w.interpolaterVars,
InterpolaterVarLock: &w.interpolaterVarLock, InterpolaterVarLock: &w.interpolaterVarLock,
@ -150,5 +150,5 @@ func (w *ContextGraphWalker) init() {
w.providerCache = make(map[string]ResourceProvider, 5) w.providerCache = make(map[string]ResourceProvider, 5)
w.providerConfigCache = make(map[string]*ResourceConfig, 5) w.providerConfigCache = make(map[string]*ResourceConfig, 5)
w.provisionerCache = make(map[string]ResourceProvisioner, 5) w.provisionerCache = make(map[string]ResourceProvisioner, 5)
w.interpolaterVars = make(map[string]map[string]string, 5) w.interpolaterVars = make(map[string]map[string]interface{}, 5)
} }

View File

@ -9,6 +9,7 @@ import (
"strings" "strings"
"sync" "sync"
"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/config/module" "github.com/hashicorp/terraform/config/module"
@ -23,12 +24,12 @@ const (
// Interpolater is the structure responsible for determining the values // Interpolater is the structure responsible for determining the values
// for interpolations such as `aws_instance.foo.bar`. // for interpolations such as `aws_instance.foo.bar`.
type Interpolater struct { type Interpolater struct {
Operation walkOperation Operation walkOperation
Module *module.Tree Module *module.Tree
State *State State *State
StateLock *sync.RWMutex StateLock *sync.RWMutex
Variables map[string]string VariableValues map[string]interface{}
VariablesLock *sync.Mutex VariableValuesLock *sync.Mutex
} }
// InterpolationScope is the current scope of execution. This is required // InterpolationScope is the current scope of execution. This is required
@ -52,12 +53,18 @@ func (i *Interpolater) Values(
mod = i.Module.Child(scope.Path[1:]) mod = i.Module.Child(scope.Path[1:])
} }
for _, v := range mod.Config().Variables { for _, v := range mod.Config().Variables {
for k, val := range v.DefaultsMap() { // Set default variables
result[k] = ast.Variable{ if v.Default == nil {
Value: val, continue
Type: ast.TypeString,
}
} }
n := fmt.Sprintf("var.%s", v.Name)
variable, err := hil.InterfaceToVariable(v.Default)
if err != nil {
return nil, fmt.Errorf("invalid default map value for %s: %v", v.Name, v.Default)
}
// Potentially TODO(jen20): check against declared type
result[n] = variable
} }
} }
@ -110,18 +117,6 @@ func (i *Interpolater) valueCountVar(
} }
} }
func interfaceToHILVariable(input interface{}) ast.Variable {
switch v := input.(type) {
case string:
return ast.Variable{
Type: ast.TypeString,
Value: v,
}
default:
panic(fmt.Errorf("Unknown interface type %T in interfaceToHILVariable", v))
}
}
func unknownVariable() ast.Variable { func unknownVariable() ast.Variable {
return ast.Variable{ return ast.Variable{
Type: ast.TypeString, Type: ast.TypeString,
@ -167,7 +162,11 @@ func (i *Interpolater) valueModuleVar(
} else { } else {
// Get the value from the outputs // Get the value from the outputs
if value, ok := mod.Outputs[v.Field]; ok { if value, ok := mod.Outputs[v.Field]; ok {
result[n] = interfaceToHILVariable(value) output, err := hil.InterfaceToVariable(value)
if err != nil {
return err
}
result[n] = output
} else { } else {
// Same reasons as the comment above. // Same reasons as the comment above.
result[n] = unknownVariable() result[n] = unknownVariable()
@ -289,33 +288,44 @@ func (i *Interpolater) valueUserVar(
n string, n string,
v *config.UserVariable, v *config.UserVariable,
result map[string]ast.Variable) error { result map[string]ast.Variable) error {
i.VariablesLock.Lock() i.VariableValuesLock.Lock()
defer i.VariablesLock.Unlock() defer i.VariableValuesLock.Unlock()
val, ok := i.Variables[v.Name] val, ok := i.VariableValues[v.Name]
if ok { if ok {
result[n] = ast.Variable{ varValue, err := hil.InterfaceToVariable(val)
Value: val, if err != nil {
Type: ast.TypeString, return fmt.Errorf("cannot convert %s value %q to an ast.Variable for interpolation: %s",
v.Name, val, err)
} }
result[n] = varValue
return nil return nil
} }
if _, ok := result[n]; !ok && i.Operation == walkValidate { if _, ok := result[n]; !ok && i.Operation == walkValidate {
result[n] = ast.Variable{ result[n] = unknownVariable()
Value: config.UnknownVariableValue,
Type: ast.TypeString,
}
return nil return nil
} }
// Look up if we have any variables with this prefix because // Look up if we have any variables with this prefix because
// those are map overrides. Include those. // those are map overrides. Include those.
for k, val := range i.Variables { for k, val := range i.VariableValues {
if strings.HasPrefix(k, v.Name+".") { if strings.HasPrefix(k, v.Name+".") {
result["var."+k] = ast.Variable{ keyComponents := strings.Split(k, ".")
Value: val, overrideKey := keyComponents[len(keyComponents)-1]
Type: ast.TypeString,
mapInterface, ok := result["var."+v.Name]
if !ok {
return fmt.Errorf("override for non-existent variable: %s", v.Name)
} }
mapVariable := mapInterface.Value.(map[string]ast.Variable)
varValue, err := hil.InterfaceToVariable(val)
if err != nil {
return fmt.Errorf("cannot convert %s value %q to an ast.Variable for interpolation: %s",
v.Name, val, err)
}
mapVariable[overrideKey] = varValue
} }
} }

View File

@ -575,7 +575,7 @@ func (m *ModuleState) Equal(other *ModuleState) bool {
return false return false
} }
for k, v := range m.Outputs { for k, v := range m.Outputs {
if other.Outputs[k] != v { if !reflect.DeepEqual(other.Outputs[k], v) {
return false return false
} }
} }
@ -803,7 +803,27 @@ func (m *ModuleState) String() string {
for _, k := range ks { for _, k := range ks {
v := m.Outputs[k] v := m.Outputs[k]
buf.WriteString(fmt.Sprintf("%s = %s\n", k, v)) switch vTyped := v.(type) {
case string:
buf.WriteString(fmt.Sprintf("%s = %s\n", k, vTyped))
case []interface{}:
buf.WriteString(fmt.Sprintf("%s = %s\n", k, vTyped))
case map[string]interface{}:
var mapKeys []string
for key, _ := range vTyped {
mapKeys = append(mapKeys, key)
}
sort.Strings(mapKeys)
var mapBuf bytes.Buffer
mapBuf.WriteString("{")
for _, key := range mapKeys {
mapBuf.WriteString(fmt.Sprintf("%s:%s ", key, vTyped[key]))
}
mapBuf.WriteString("}")
buf.WriteString(fmt.Sprintf("%s = %s\n", k, mapBuf.String()))
}
} }
} }

View File

@ -592,7 +592,7 @@ aws_instance.foo:
Outputs: Outputs:
foo_num = bar,bar,bar foo_num = [bar,bar,bar]
` `
const testTerraformApplyOutputMultiStr = ` const testTerraformApplyOutputMultiStr = `

View File

@ -0,0 +1,9 @@
variable "amis" {
type = "map"
}
resource "null_resource" "noop" {}
output "amis_out" {
value = "${var.amis}"
}

View File

@ -0,0 +1,19 @@
variable "amis_in" {
type = "map"
default = {
"us-west-1" = "ami-123456"
"us-west-2" = "ami-456789"
"eu-west-1" = "ami-789012"
"eu-west-2" = "ami-989484"
}
}
module "test" {
source = "./amodule"
amis = "${var.amis_in}"
}
output "amis_from_module" {
value = "${module.test.amis_out}"
}

View File

@ -19,5 +19,5 @@ resource "aws_instance" "foo" {
resource "aws_instance" "bar" { resource "aws_instance" "bar" {
foo = "${var.foo}" foo = "${var.foo}"
bar = "${lookup(var.amis, var.foo)}" bar = "${lookup(var.amis, var.foo)}"
baz = "${var.amis.us-east-1}" baz = "${var.amis["us-east-1"]}"
} }

View File

@ -122,6 +122,7 @@ support for the "us-west-2" region as well:
``` ```
variable "amis" { variable "amis" {
type = "map"
default = { default = {
us-east-1 = "ami-b8b061d0" us-east-1 = "ami-b8b061d0"
us-west-2 = "ami-ef5e24df" us-west-2 = "ami-ef5e24df"
@ -129,8 +130,8 @@ variable "amis" {
} }
``` ```
A variable becomes a mapping when it has a default value that is a A variable becomes a mapping when it has a type of "map" assigned, or has a
map like above. There is no way to create a required map. default value that is a map like above.
Then, replace the "aws\_instance" with the following: Then, replace the "aws\_instance" with the following:
@ -148,7 +149,7 @@ variables is the key.
While we don't use it in our example, it is worth noting that you While we don't use it in our example, it is worth noting that you
can also do a static lookup of a mapping directly with can also do a static lookup of a mapping directly with
`${var.amis.us-east-1}`. `${var.amis["us-east-1"]}`.
<a id="assigning-mappings"></a> <a id="assigning-mappings"></a>
## Assigning Mappings ## Assigning Mappings