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
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 = <sensitive>\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 = <sensitive>\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")
}
}
}

View File

@ -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]

View File

@ -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

View File

@ -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 {

View File

@ -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
},
}
}

View File

@ -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,
},

View File

@ -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

View File

@ -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
}

View File

@ -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 {

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) {
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)
}
}

View File

@ -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)
}
}

View File

@ -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.

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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,
},
},
}

View File

@ -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{

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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()))
}
}
}

View File

@ -592,7 +592,7 @@ aws_instance.foo:
Outputs:
foo_num = bar,bar,bar
foo_num = [bar,bar,bar]
`
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" {
foo = "${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" {
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"]}`.
<a id="assigning-mappings"></a>
## Assigning Mappings