diff --git a/command/apply.go b/command/apply.go index 9d0c3956a..c61f84d4e 100644 --- a/command/apply.go +++ b/command/apply.go @@ -394,29 +394,31 @@ func outputsAsString(state *terraform.State, schema []*config.Output) string { // Output the outputs in alphabetical order keyLen := 0 - keys := make([]string, 0, len(outputs)) + ks := make([]string, 0, len(outputs)) for key, _ := range outputs { - keys = append(keys, key) + ks = append(ks, key) if len(key) > keyLen { keyLen = len(key) } } - sort.Strings(keys) - - for _, k := range keys { - v := outputs[k] + sort.Strings(ks) + for _, k := range ks { if schemaMap[k].Sensitive { - outputBuf.WriteString(fmt.Sprintf( - " %s%s = \n", - k, - strings.Repeat(" ", keyLen-len(k)))) - } else { - outputBuf.WriteString(fmt.Sprintf( - " %s%s = %s\n", - k, - strings.Repeat(" ", keyLen-len(k)), - v)) + outputBuf.WriteString(fmt.Sprintf("%s = \n", k)) + continue + } + + v := outputs[k] + switch typedV := v.(type) { + case string: + outputBuf.WriteString(fmt.Sprintf("%s = %s\n", k, typedV)) + case []interface{}: + outputBuf.WriteString(formatListOutput("", k, typedV)) + outputBuf.WriteString("\n") + case map[string]interface{}: + outputBuf.WriteString(formatMapOutput("", k, typedV)) + outputBuf.WriteString("\n") } } } diff --git a/command/output.go b/command/output.go index 420dd438b..d031dfb3e 100644 --- a/command/output.go +++ b/command/output.go @@ -1,9 +1,11 @@ package command import ( + "bytes" "flag" "fmt" "sort" + "strconv" "strings" ) @@ -27,7 +29,7 @@ func (c *OutputCommand) Run(args []string) int { } args = cmdFlags.Args() - if len(args) > 1 { + if len(args) > 2 { c.Ui.Error( "The output command expects exactly one argument with the name\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] } + index := "" + if len(args) > 1 { + index = args[1] + } + stateStore, err := c.Meta.State() if err != nil { c.Ui.Error(fmt.Sprintf("Error reading state: %s", err)) @@ -74,17 +81,7 @@ func (c *OutputCommand) Run(args []string) int { } if name == "" { - ks := make([]string, 0, len(mod.Outputs)) - 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)) - } + c.Ui.Output(outputsAsString(state)) return 0 } @@ -101,6 +98,44 @@ func (c *OutputCommand) Run(args []string) int { switch output := v.(type) { case string: 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: panic(fmt.Errorf("Unknown output type: %T", output)) } @@ -108,6 +143,53 @@ func (c *OutputCommand) Run(args []string) int { 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 { helpText := ` Usage: terraform output [options] [NAME] diff --git a/config/config.go b/config/config.go index b3a48be1d..e3eb6530e 100644 --- a/config/config.go +++ b/config/config.go @@ -11,7 +11,6 @@ import ( "github.com/hashicorp/go-multierror" "github.com/hashicorp/hil" "github.com/hashicorp/hil/ast" - "github.com/hashicorp/terraform/flatmap" "github.com/mitchellh/mapstructure" "github.com/mitchellh/reflectwalk" ) @@ -239,7 +238,7 @@ func (c *Config) Validate() error { } interp := false - fn := func(ast.Node) (string, error) { + fn := func(ast.Node) (interface{}, error) { interp = true return "", nil } @@ -450,7 +449,7 @@ func (c *Config) Validate() error { } // 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 // a fixed value of "5" for all interpolations. result, err := hil.Eval( @@ -461,7 +460,7 @@ func (c *Config) Validate() error { return "", err } - return result.Value.(string), nil + return result.Value, nil }) _, err := strconv.ParseInt(r.RawCount.Value().(string), 0, 0) if err != nil { @@ -809,28 +808,6 @@ func (r *Resource) mergerMerge(m merger) merger { 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. func (v *Variable) Merge(v2 *Variable) *Variable { // Shallow copy the variable diff --git a/config/config_test.go b/config/config_test.go index b6303fb13..8e2565ff4 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -2,7 +2,6 @@ package config import ( "path/filepath" - "reflect" "strings" "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 { c, err := LoadFile(filepath.Join(fixtureDir, name, "main.tf")) if err != nil { diff --git a/config/interpolate_funcs.go b/config/interpolate_funcs.go index 9f929e106..53d8ccccb 100644 --- a/config/interpolate_funcs.go +++ b/config/interpolate_funcs.go @@ -19,6 +19,7 @@ import ( "github.com/apparentlymart/go-cidr/cidr" "github.com/hashicorp/go-uuid" + "github.com/hashicorp/hil" "github.com/hashicorp/hil/ast" "github.com/mitchellh/go-homedir" ) @@ -466,20 +467,22 @@ func interpolationFuncSplit() ast.Function { // dynamic lookups of map types within a Terraform configuration. func interpolationFuncLookup(vs map[string]ast.Variable) ast.Function { return ast.Function{ - ArgTypes: []ast.Type{ast.TypeString, ast.TypeString}, + ArgTypes: []ast.Type{ast.TypeMap, ast.TypeString}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { - k := fmt.Sprintf("var.%s.%s", args[0].(string), args[1].(string)) - v, ok := vs[k] + index := args[1].(string) + mapVar := args[0].(map[string]ast.Variable) + + v, ok := mapVar[index] if !ok { return "", fmt.Errorf( - "lookup in '%s' failed to find '%s'", - args[0].(string), args[1].(string)) + "lookup failed to find '%s'", + 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) + "lookup for '%s' has bad type %s", + args[1].(string), v.Type) } return v.Value.(string), nil @@ -513,28 +516,24 @@ func interpolationFuncElement() ast.Function { // keys of map types within a Terraform configuration. func interpolationFuncKeys(vs map[string]ast.Variable) ast.Function { return ast.Function{ - ArgTypes: []ast.Type{ast.TypeString}, - ReturnType: ast.TypeString, + ArgTypes: []ast.Type{ast.TypeMap}, + ReturnType: ast.TypeList, Callback: func(args []interface{}) (interface{}, error) { - // Prefix must include ending dot to be a map - prefix := fmt.Sprintf("var.%s.", args[0].(string)) - keys := make([]string, 0, len(vs)) - for k, _ := range vs { - if !strings.HasPrefix(k, prefix) { - continue - } - keys = append(keys, k[len(prefix):]) - } + mapVar := args[0].(map[string]ast.Variable) + keys := make([]string, 0) - if len(keys) <= 0 { - return "", fmt.Errorf( - "failed to find map '%s'", - args[0].(string)) + for k, _ := range mapVar { + keys = append(keys, k) } 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. func interpolationFuncValues(vs map[string]ast.Variable) ast.Function { return ast.Function{ - ArgTypes: []ast.Type{ast.TypeString}, - ReturnType: ast.TypeString, + ArgTypes: []ast.Type{ast.TypeMap}, + ReturnType: ast.TypeList, Callback: func(args []interface{}) (interface{}, error) { - // Prefix must include ending dot to be a map - prefix := fmt.Sprintf("var.%s.", args[0].(string)) - keys := make([]string, 0, len(vs)) - for k, _ := range vs { - if !strings.HasPrefix(k, prefix) { - continue - } - keys = append(keys, k) - } + mapVar := args[0].(map[string]ast.Variable) + keys := make([]string, 0) - if len(keys) <= 0 { - return "", fmt.Errorf( - "failed to find map '%s'", - args[0].(string)) + for k, _ := range mapVar { + keys = append(keys, k) } sort.Strings(keys) - vals := make([]string, 0, len(keys)) - - for _, k := range keys { - v := vs[k] - if v.Type != ast.TypeString { - return "", fmt.Errorf("values(): %q has bad type %s", k, v.Type) + values := make([]string, len(keys)) + for index, key := range keys { + if value, ok := mapVar[key].Value.(string); ok { + values[index] = value + } else { + 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 }, } } diff --git a/config/interpolate_funcs_test.go b/config/interpolate_funcs_test.go index 774a7bf45..e7b2cfa84 100644 --- a/config/interpolate_funcs_test.go +++ b/config/interpolate_funcs_test.go @@ -672,28 +672,33 @@ func TestInterpolateFuncSplit(t *testing.T) { func TestInterpolateFuncLookup(t *testing.T) { testFunction(t, testFunctionConfig{ Vars: map[string]ast.Variable{ - "var.foo.bar": ast.Variable{ - Value: "baz", - Type: ast.TypeString, + "var.foo": ast.Variable{ + Type: ast.TypeMap, + Value: map[string]ast.Variable{ + "bar": ast.Variable{ + Type: ast.TypeString, + Value: "baz", + }, + }, }, }, Cases: []testFunctionCase{ { - `${lookup("foo", "bar")}`, + `${lookup(var.foo, "bar")}`, "baz", false, }, // Invalid key { - `${lookup("foo", "baz")}`, + `${lookup(var.foo, "baz")}`, nil, true, }, // Too many args { - `${lookup("foo", "bar", "baz")}`, + `${lookup(var.foo, "bar", "baz")}`, nil, true, }, @@ -704,13 +709,18 @@ func TestInterpolateFuncLookup(t *testing.T) { func TestInterpolateFuncKeys(t *testing.T) { testFunction(t, testFunctionConfig{ Vars: map[string]ast.Variable{ - "var.foo.bar": ast.Variable{ - Value: "baz", - Type: ast.TypeString, - }, - "var.foo.qux": ast.Variable{ - Value: "quack", - Type: ast.TypeString, + "var.foo": ast.Variable{ + Type: ast.TypeMap, + Value: map[string]ast.Variable{ + "bar": ast.Variable{ + Value: "baz", + Type: ast.TypeString, + }, + "qux": ast.Variable{ + Value: "quack", + Type: ast.TypeString, + }, + }, }, "var.str": ast.Variable{ Value: "astring", @@ -719,28 +729,28 @@ func TestInterpolateFuncKeys(t *testing.T) { }, Cases: []testFunctionCase{ { - `${keys("foo")}`, - NewStringList([]string{"bar", "qux"}).String(), + `${keys(var.foo)}`, + []interface{}{"bar", "qux"}, false, }, // Invalid key { - `${keys("not")}`, + `${keys(var.not)}`, nil, true, }, // Too many args { - `${keys("foo", "bar")}`, + `${keys(var.foo, "bar")}`, nil, true, }, // Not a map { - `${keys("str")}`, + `${keys(var.str)}`, nil, true, }, @@ -751,13 +761,18 @@ func TestInterpolateFuncKeys(t *testing.T) { func TestInterpolateFuncValues(t *testing.T) { testFunction(t, testFunctionConfig{ Vars: map[string]ast.Variable{ - "var.foo.bar": ast.Variable{ - Value: "quack", - Type: ast.TypeString, - }, - "var.foo.qux": ast.Variable{ - Value: "baz", - Type: ast.TypeString, + "var.foo": ast.Variable{ + Type: ast.TypeMap, + Value: map[string]ast.Variable{ + "bar": ast.Variable{ + Value: "quack", + Type: ast.TypeString, + }, + "qux": ast.Variable{ + Value: "baz", + Type: ast.TypeString, + }, + }, }, "var.str": ast.Variable{ Value: "astring", @@ -766,28 +781,28 @@ func TestInterpolateFuncValues(t *testing.T) { }, Cases: []testFunctionCase{ { - `${values("foo")}`, - NewStringList([]string{"quack", "baz"}).String(), + `${values(var.foo)}`, + []interface{}{"quack", "baz"}, false, }, // Invalid key { - `${values("not")}`, + `${values(var.not)}`, nil, true, }, // Too many args { - `${values("foo", "bar")}`, + `${values(var.foo, "bar")}`, nil, true, }, // Not a map { - `${values("str")}`, + `${values(var.str)}`, nil, true, }, diff --git a/config/interpolate_walk.go b/config/interpolate_walk.go index 333cf33ed..eb5c2480a 100644 --- a/config/interpolate_walk.go +++ b/config/interpolate_walk.go @@ -42,7 +42,7 @@ type interpolationWalker struct { // // If Replace is set to false in interpolationWalker, then the replace // 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 // 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 // splitting (in a SliceElem) or not. remove := false - if w.loc == reflectwalk.SliceElem && IsStringList(replaceVal) { - parts := StringList(replaceVal).Slice() + if w.loc == reflectwalk.SliceElem && IsStringList(replaceVal.(string)) { + parts := StringList(replaceVal.(string)).Slice() for _, p := range parts { if p == UnknownVariableValue { remove = true diff --git a/config/interpolate_walk_test.go b/config/interpolate_walk_test.go index b7c308cd4..e00eefe72 100644 --- a/config/interpolate_walk_test.go +++ b/config/interpolate_walk_test.go @@ -89,7 +89,7 @@ func TestInterpolationWalker_detect(t *testing.T) { for i, tc := range cases { var actual []string - detectFn := func(root ast.Node) (string, error) { + detectFn := func(root ast.Node) (interface{}, error) { actual = append(actual, fmt.Sprintf("%s", root)) return "", nil } @@ -175,7 +175,7 @@ func TestInterpolationWalker_replace(t *testing.T) { } for i, tc := range cases { - fn := func(ast.Node) (string, error) { + fn := func(ast.Node) (interface{}, error) { return tc.Value, nil } diff --git a/config/raw_config.go b/config/raw_config.go index 6fc15ebd5..18b9dcaf2 100644 --- a/config/raw_config.go +++ b/config/raw_config.go @@ -108,7 +108,7 @@ func (r *RawConfig) Interpolate(vs map[string]ast.Variable) error { defer r.lock.Unlock() 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 // of the variables is the computed value. If it is, then we // treat this entire value as computed. @@ -137,7 +137,7 @@ func (r *RawConfig) Interpolate(vs map[string]ast.Variable) error { return "", err } - return result.Value.(string), nil + return result.Value, nil }) } @@ -194,7 +194,7 @@ func (r *RawConfig) init() error { r.Interpolations = nil r.Variables = nil - fn := func(node ast.Node) (string, error) { + fn := func(node ast.Node) (interface{}, error) { r.Interpolations = append(r.Interpolations, node) vars, err := DetectVariables(node) if err != nil { diff --git a/terraform/context_apply_test.go b/terraform/context_apply_test.go index 3485b13bf..574562c4f 100644 --- a/terraform/context_apply_test.go +++ b/terraform/context_apply_test.go @@ -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(` +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) { m := testModule(t, "apply-provider-alias") p := testProvider("aws") @@ -3066,7 +3105,7 @@ func TestContext2Apply_outputInvalid(t *testing.T) { if err == nil { 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) } } @@ -3144,7 +3183,7 @@ func TestContext2Apply_outputList(t *testing.T) { actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyOutputListStr) 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()) expected := strings.TrimSpace(testTerraformApplyVarsStr) if actual != expected { - t.Fatalf("bad: \n%s", actual) + t.Fatalf("expected: %s\n got:\n%s", expected, actual) } } diff --git a/terraform/context_input_test.go b/terraform/context_input_test.go index 404ef0ffc..dae45e0d0 100644 --- a/terraform/context_input_test.go +++ b/terraform/context_input_test.go @@ -45,7 +45,7 @@ func TestContext2Input(t *testing.T) { actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformInputVarsStr) if actual != expected { - t.Fatalf("bad: \n%s", actual) + t.Fatalf("expected:\n%s\ngot:\n%s", expected, actual) } } diff --git a/terraform/eval_context.go b/terraform/eval_context.go index f4427939a..f2867511d 100644 --- a/terraform/eval_context.go +++ b/terraform/eval_context.go @@ -68,7 +68,7 @@ type EvalContext interface { // SetVariables sets the variables for the module within // this context with the name n. This function call is additive: // 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 // be used to modify that diff. diff --git a/terraform/eval_context_builtin.go b/terraform/eval_context_builtin.go index a25c1c6a1..4dff93a4c 100644 --- a/terraform/eval_context_builtin.go +++ b/terraform/eval_context_builtin.go @@ -23,7 +23,7 @@ type BuiltinEvalContext struct { // as the Interpolater itself, it is protected by InterpolaterVarLock // which must be locked during any access to the map. Interpolater *Interpolater - InterpolaterVars map[string]map[string]string + InterpolaterVars map[string]map[string]interface{} InterpolaterVarLock *sync.Mutex Hooks []Hook @@ -311,7 +311,7 @@ func (ctx *BuiltinEvalContext) Path() []string { 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() defer ctx.InterpolaterVarLock.Unlock() @@ -322,7 +322,7 @@ func (ctx *BuiltinEvalContext) SetVariables(n string, vs map[string]string) { vars := ctx.InterpolaterVars[key] if vars == nil { - vars = make(map[string]string) + vars = make(map[string]interface{}) ctx.InterpolaterVars[key] = vars } diff --git a/terraform/eval_context_mock.go b/terraform/eval_context_mock.go index 60d83c724..4f5c23bc4 100644 --- a/terraform/eval_context_mock.go +++ b/terraform/eval_context_mock.go @@ -74,7 +74,7 @@ type MockEvalContext struct { SetVariablesCalled bool SetVariablesModule string - SetVariablesVariables map[string]string + SetVariablesVariables map[string]interface{} DiffCalled bool DiffDiff *Diff @@ -183,7 +183,7 @@ func (c *MockEvalContext) Path() []string { 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.SetVariablesModule = n c.SetVariablesVariables = vs diff --git a/terraform/eval_output.go b/terraform/eval_output.go index acdc268c3..b584bdecc 100644 --- a/terraform/eval_output.go +++ b/terraform/eval_output.go @@ -2,6 +2,7 @@ package terraform import ( "fmt" + "log" "github.com/hashicorp/terraform/config" ) @@ -45,7 +46,8 @@ type EvalWriteOutput struct { func (n *EvalWriteOutput) Eval(ctx EvalContext) (interface{}, error) { cfg, err := ctx.Interpolate(n.Value, 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() @@ -76,16 +78,16 @@ func (n *EvalWriteOutput) Eval(ctx EvalContext) (interface{}, error) { } } - // If it is a list of values, get the first one - if list, ok := valueRaw.([]interface{}); ok { - valueRaw = list[0] + switch valueTyped := valueRaw.(type) { + case string: + 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 } diff --git a/terraform/eval_variable.go b/terraform/eval_variable.go index 216efe5b8..f5f60f0e9 100644 --- a/terraform/eval_variable.go +++ b/terraform/eval_variable.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - "github.com/hashicorp/errwrap" "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config/module" "github.com/mitchellh/mapstructure" @@ -26,7 +25,7 @@ import ( // 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. type EvalTypeCheckVariable struct { - Variables map[string]string + Variables map[string]interface{} ModulePath []string ModuleTree *module.Tree } @@ -43,12 +42,18 @@ func (n *EvalTypeCheckVariable) Eval(ctx EvalContext) (interface{}, error) { 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 { // 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 // of strings and maps - where the only valid inter-module variable type is // string. - _, ok := n.Variables[name] + proposedValue, ok := n.Variables[name] if !ok { // This means the default value should be used as no overriding value // 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: // This will need actual verification once we aren't dealing with // a map[string]string but this is sufficient for now. - continue - default: - // Only display a module 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 = "" + switch proposedValue.(type) { + case string: + continue + default: + return nil, fmt.Errorf("variable %s%s should be type %s, got %T", + 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 // just strings and maps. 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. type EvalSetVariables struct { Module *string - Variables map[string]string + Variables map[string]interface{} } // 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 // mapping. type EvalVariableBlock struct { - Config **ResourceConfig - Variables map[string]string + Config **ResourceConfig + VariableValues map[string]interface{} } // TODO: test func (n *EvalVariableBlock) Eval(ctx EvalContext) (interface{}, error) { // Clear out the existing mapping - for k, _ := range n.Variables { - delete(n.Variables, k) + for k, _ := range n.VariableValues { + delete(n.VariableValues, k) } // Get our configuration rc := *n.Config for k, v := range rc.Config { - var vStr string - if err := mapstructure.WeakDecode(v, &vStr); err != nil { - return nil, errwrap.Wrapf(fmt.Sprintf( - "%s: error reading value: {{err}}", k), err) + var vString string + if err := mapstructure.WeakDecode(v, &vString); err == nil { + n.VariableValues[k] = vString + 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 { - if _, ok := n.Variables[k]; !ok { - n.Variables[k] = config.UnknownVariableValue + if _, ok := n.VariableValues[k]; !ok { + n.VariableValues[k] = config.UnknownVariableValue } } diff --git a/terraform/graph_config_node_module.go b/terraform/graph_config_node_module.go index ba377e94d..3e36e1ea5 100644 --- a/terraform/graph_config_node_module.go +++ b/terraform/graph_config_node_module.go @@ -69,7 +69,7 @@ func (n *GraphNodeConfigModule) Expand(b GraphBuilder) (GraphNodeSubgraph, error return &graphNodeModuleExpanded{ Original: n, Graph: graph, - Variables: make(map[string]string), + Variables: make(map[string]interface{}), }, nil } @@ -107,7 +107,7 @@ type graphNodeModuleExpanded struct { // Variables is a map of the input variables. This reference should // be shared with ModuleInputTransformer in order to create a connection // where the variables are set properly. - Variables map[string]string + Variables map[string]interface{} } func (n *graphNodeModuleExpanded) Name() string { @@ -147,8 +147,8 @@ func (n *graphNodeModuleExpanded) EvalTree() EvalNode { }, &EvalVariableBlock{ - Config: &resourceConfig, - Variables: n.Variables, + Config: &resourceConfig, + VariableValues: n.Variables, }, }, } diff --git a/terraform/graph_config_node_variable.go b/terraform/graph_config_node_variable.go index e462070d0..389d7babf 100644 --- a/terraform/graph_config_node_variable.go +++ b/terraform/graph_config_node_variable.go @@ -114,7 +114,7 @@ func (n *GraphNodeConfigVariable) EvalTree() EvalNode { // Otherwise, interpolate the value of this variable and set it // within the variables mapping. var config *ResourceConfig - variables := make(map[string]string) + variables := make(map[string]interface{}) return &EvalSequence{ Nodes: []EvalNode{ &EvalInterpolate{ @@ -123,8 +123,8 @@ func (n *GraphNodeConfigVariable) EvalTree() EvalNode { }, &EvalVariableBlock{ - Config: &config, - Variables: variables, + Config: &config, + VariableValues: variables, }, &EvalTypeCheckVariable{ diff --git a/terraform/graph_walk_context.go b/terraform/graph_walk_context.go index ac6310d08..7424fdbbd 100644 --- a/terraform/graph_walk_context.go +++ b/terraform/graph_walk_context.go @@ -27,7 +27,7 @@ type ContextGraphWalker struct { once sync.Once contexts map[string]*BuiltinEvalContext contextLock sync.Mutex - interpolaterVars map[string]map[string]string + interpolaterVars map[string]map[string]interface{} interpolaterVarLock sync.Mutex providerCache map[string]ResourceProvider providerConfigCache map[string]*ResourceConfig @@ -49,7 +49,7 @@ func (w *ContextGraphWalker) EnterPath(path []string) EvalContext { } // Setup the variables for this interpolater - variables := make(map[string]string) + variables := make(map[string]interface{}) if len(path) <= 1 { for k, v := range w.Context.variables { variables[k] = v @@ -81,12 +81,12 @@ func (w *ContextGraphWalker) EnterPath(path []string) EvalContext { StateValue: w.Context.state, StateLock: &w.Context.stateLock, Interpolater: &Interpolater{ - Operation: w.Operation, - Module: w.Context.module, - State: w.Context.state, - StateLock: &w.Context.stateLock, - Variables: variables, - VariablesLock: &w.interpolaterVarLock, + Operation: w.Operation, + Module: w.Context.module, + State: w.Context.state, + StateLock: &w.Context.stateLock, + VariableValues: variables, + VariableValuesLock: &w.interpolaterVarLock, }, InterpolaterVars: w.interpolaterVars, InterpolaterVarLock: &w.interpolaterVarLock, @@ -150,5 +150,5 @@ func (w *ContextGraphWalker) init() { w.providerCache = make(map[string]ResourceProvider, 5) w.providerConfigCache = make(map[string]*ResourceConfig, 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) } diff --git a/terraform/interpolate.go b/terraform/interpolate.go index 2dec59adc..66f36246c 100644 --- a/terraform/interpolate.go +++ b/terraform/interpolate.go @@ -9,6 +9,7 @@ import ( "strings" "sync" + "github.com/hashicorp/hil" "github.com/hashicorp/hil/ast" "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config/module" @@ -23,12 +24,12 @@ const ( // Interpolater is the structure responsible for determining the values // for interpolations such as `aws_instance.foo.bar`. type Interpolater struct { - Operation walkOperation - Module *module.Tree - State *State - StateLock *sync.RWMutex - Variables map[string]string - VariablesLock *sync.Mutex + Operation walkOperation + Module *module.Tree + State *State + StateLock *sync.RWMutex + VariableValues map[string]interface{} + VariableValuesLock *sync.Mutex } // 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:]) } for _, v := range mod.Config().Variables { - for k, val := range v.DefaultsMap() { - result[k] = ast.Variable{ - Value: val, - Type: ast.TypeString, - } + // Set default variables + if v.Default == nil { + continue } + + 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 { return ast.Variable{ Type: ast.TypeString, @@ -167,7 +162,11 @@ func (i *Interpolater) valueModuleVar( } else { // Get the value from the outputs 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 { // Same reasons as the comment above. result[n] = unknownVariable() @@ -289,33 +288,44 @@ func (i *Interpolater) valueUserVar( n string, v *config.UserVariable, result map[string]ast.Variable) error { - i.VariablesLock.Lock() - defer i.VariablesLock.Unlock() - val, ok := i.Variables[v.Name] + i.VariableValuesLock.Lock() + defer i.VariableValuesLock.Unlock() + val, ok := i.VariableValues[v.Name] if ok { - result[n] = ast.Variable{ - Value: val, - Type: ast.TypeString, + 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) } + result[n] = varValue return nil } if _, ok := result[n]; !ok && i.Operation == walkValidate { - result[n] = ast.Variable{ - Value: config.UnknownVariableValue, - Type: ast.TypeString, - } + result[n] = unknownVariable() return nil } // Look up if we have any variables with this prefix because // those are map overrides. Include those. - for k, val := range i.Variables { + for k, val := range i.VariableValues { if strings.HasPrefix(k, v.Name+".") { - result["var."+k] = ast.Variable{ - Value: val, - Type: ast.TypeString, + keyComponents := strings.Split(k, ".") + overrideKey := keyComponents[len(keyComponents)-1] + + 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 } } diff --git a/terraform/state.go b/terraform/state.go index f7c7f9c5a..fd853adf1 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -575,7 +575,7 @@ func (m *ModuleState) Equal(other *ModuleState) bool { return false } for k, v := range m.Outputs { - if other.Outputs[k] != v { + if !reflect.DeepEqual(other.Outputs[k], v) { return false } } @@ -803,7 +803,27 @@ func (m *ModuleState) String() string { for _, k := range ks { 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())) + } } } diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index 09fb2370c..ad19d7b1b 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -592,7 +592,7 @@ aws_instance.foo: Outputs: -foo_num = bar,bar,bar +foo_num = [bar,bar,bar] ` const testTerraformApplyOutputMultiStr = ` diff --git a/terraform/test-fixtures/apply-map-var-through-module/amodule/main.tf b/terraform/test-fixtures/apply-map-var-through-module/amodule/main.tf new file mode 100644 index 000000000..133ac62fc --- /dev/null +++ b/terraform/test-fixtures/apply-map-var-through-module/amodule/main.tf @@ -0,0 +1,9 @@ +variable "amis" { + type = "map" +} + +resource "null_resource" "noop" {} + +output "amis_out" { + value = "${var.amis}" +} diff --git a/terraform/test-fixtures/apply-map-var-through-module/main.tf b/terraform/test-fixtures/apply-map-var-through-module/main.tf new file mode 100644 index 000000000..991a0ecf6 --- /dev/null +++ b/terraform/test-fixtures/apply-map-var-through-module/main.tf @@ -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}" +} diff --git a/terraform/test-fixtures/apply-vars/main.tf b/terraform/test-fixtures/apply-vars/main.tf index 01ffb6a91..7cd4b5316 100644 --- a/terraform/test-fixtures/apply-vars/main.tf +++ b/terraform/test-fixtures/apply-vars/main.tf @@ -19,5 +19,5 @@ resource "aws_instance" "foo" { resource "aws_instance" "bar" { foo = "${var.foo}" bar = "${lookup(var.amis, var.foo)}" - baz = "${var.amis.us-east-1}" + baz = "${var.amis["us-east-1"]}" } diff --git a/website/source/intro/getting-started/variables.html.md b/website/source/intro/getting-started/variables.html.md index a9dcc15db..0b2668259 100644 --- a/website/source/intro/getting-started/variables.html.md +++ b/website/source/intro/getting-started/variables.html.md @@ -122,6 +122,7 @@ support for the "us-west-2" region as well: ``` variable "amis" { + type = "map" default = { us-east-1 = "ami-b8b061d0" 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 -map like above. There is no way to create a required map. +A variable becomes a mapping when it has a type of "map" assigned, or has a +default value that is a map like above. 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 can also do a static lookup of a mapping directly with -`${var.amis.us-east-1}`. +`${var.amis["us-east-1"]}`. ## Assigning Mappings