From e57a399d718cb232ea6e480b6ea890b85f47e10b Mon Sep 17 00:00:00 2001 From: James Nugent Date: Mon, 11 Apr 2016 12:40:06 -0500 Subject: [PATCH] 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. --- command/apply.go | 34 +++--- command/output.go | 106 ++++++++++++++++-- config/config.go | 29 +---- config/config_test.go | 38 ------- config/interpolate_funcs.go | 87 +++++++------- config/interpolate_funcs_test.go | 75 ++++++++----- config/interpolate_walk.go | 6 +- config/interpolate_walk_test.go | 4 +- config/raw_config.go | 6 +- terraform/context_apply_test.go | 45 +++++++- terraform/context_input_test.go | 2 +- terraform/eval_context.go | 2 +- terraform/eval_context_builtin.go | 6 +- terraform/eval_context_mock.go | 4 +- terraform/eval_output.go | 22 ++-- terraform/eval_variable.go | 69 ++++++++---- terraform/graph_config_node_module.go | 8 +- terraform/graph_config_node_variable.go | 6 +- terraform/graph_walk_context.go | 18 +-- terraform/interpolate.go | 86 +++++++------- terraform/state.go | 24 +++- terraform/terraform_test.go | 2 +- .../amodule/main.tf | 9 ++ .../apply-map-var-through-module/main.tf | 19 ++++ terraform/test-fixtures/apply-vars/main.tf | 2 +- .../intro/getting-started/variables.html.md | 7 +- 26 files changed, 438 insertions(+), 278 deletions(-) create mode 100644 terraform/test-fixtures/apply-map-var-through-module/amodule/main.tf create mode 100644 terraform/test-fixtures/apply-map-var-through-module/main.tf 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