diff --git a/command/flag_kv.go b/command/flag_kv.go index 789c44c999a7..a618001df87d 100644 --- a/command/flag_kv.go +++ b/command/flag_kv.go @@ -112,6 +112,10 @@ func loadKVFile(rawPath string) (map[string]interface{}, error) { "Decoding errors are usually caused by an invalid format.", err) } + err = flattenMultiMaps(result) + if err != nil { + return nil, err + } return result, nil } @@ -202,10 +206,34 @@ func parseVarFlagAsHCL(input string) (string, interface{}, error) { return "", nil, fmt.Errorf("Cannot parse value for variable %s (%q) as valid HCL. Only one value may be specified.", probablyName, input) } - for k, v := range decoded { - return k, v, nil + err = flattenMultiMaps(decoded) + if err != nil { + return probablyName, "", err } - // Should be unreachable - return "", nil, fmt.Errorf("No value for variable: %s", input) + var k string + var v interface{} + for k, v = range decoded { + break + } + return k, v, nil +} + +// Variables don't support any type that can be configured via multiple +// declarations of the same HCL map, so any instances of +// []map[string]interface{} are either a single map that can be flattened, or +// are invalid config. +func flattenMultiMaps(m map[string]interface{}) error { + for k, v := range m { + switch v := v.(type) { + case []map[string]interface{}: + switch { + case len(v) > 1: + return fmt.Errorf("multiple map declarations not supported for variables") + case len(v) == 1: + m[k] = v[0] + } + } + } + return nil } diff --git a/command/flag_kv_test.go b/command/flag_kv_test.go index e1df046e8451..de5dd9d84a58 100644 --- a/command/flag_kv_test.go +++ b/command/flag_kv_test.go @@ -119,11 +119,9 @@ func TestFlagTypedKV(t *testing.T) { { `key={"hello" = "world", "foo" = "bar"}`, map[string]interface{}{ - "key": []map[string]interface{}{ - map[string]interface{}{ - "hello": "world", - "foo": "bar", - }, + "key": map[string]interface{}{ + "hello": "world", + "foo": "bar", }, }, false, @@ -193,6 +191,10 @@ func TestFlagKVFile(t *testing.T) { inputLibucl := ` foo = "bar" ` + inputMap := ` +foo = { + k = "v" +}` inputJson := `{ "foo": "bar"}` @@ -219,6 +221,16 @@ foo = "bar" map[string]interface{}{"map.key": "foo"}, false, }, + + { + inputMap, + map[string]interface{}{ + "foo": map[string]interface{}{ + "k": "v", + }, + }, + false, + }, } path := testTempFile(t) diff --git a/terraform/eval_output.go b/terraform/eval_output.go index bee4f10848c5..cf61781e5b91 100644 --- a/terraform/eval_output.go +++ b/terraform/eval_output.go @@ -98,6 +98,19 @@ func (n *EvalWriteOutput) Eval(ctx EvalContext) (interface{}, error) { Sensitive: n.Sensitive, Value: valueTyped, } + case []map[string]interface{}: + // an HCL map is multi-valued, so if this was read out of a config the + // map may still be in a slice. + if len(valueTyped) == 1 { + mod.Outputs[n.Name] = &OutputState{ + Type: "map", + Sensitive: n.Sensitive, + Value: valueTyped[0], + } + break + } + return nil, fmt.Errorf("output %s type (%T) with %d values not valid for type map", + n.Name, valueTyped, len(valueTyped)) default: return nil, fmt.Errorf("output %s is not a valid type (%T)\n", n.Name, valueTyped) } diff --git a/terraform/eval_output_test.go b/terraform/eval_output_test.go new file mode 100644 index 000000000000..f73b127de77f --- /dev/null +++ b/terraform/eval_output_test.go @@ -0,0 +1,56 @@ +package terraform + +import ( + "sync" + "testing" +) + +func TestEvalWriteMapOutput(t *testing.T) { + ctx := new(MockEvalContext) + ctx.StateState = NewState() + ctx.StateLock = new(sync.RWMutex) + + cases := []struct { + name string + cfg *ResourceConfig + err bool + }{ + { + // Eval should recognize a single map in a slice, and collapse it + // into the map value + "single-map", + &ResourceConfig{ + Config: map[string]interface{}{ + "value": []map[string]interface{}{ + map[string]interface{}{"a": "b"}, + }, + }, + }, + false, + }, + { + // we can't apply a multi-valued map to a variable, so this should error + "multi-map", + &ResourceConfig{ + Config: map[string]interface{}{ + "value": []map[string]interface{}{ + map[string]interface{}{"a": "b"}, + map[string]interface{}{"c": "d"}, + }, + }, + }, + true, + }, + } + + for _, tc := range cases { + evalNode := &EvalWriteOutput{Name: tc.name} + ctx.InterpolateConfigResult = tc.cfg + t.Run(tc.name, func(t *testing.T) { + _, err := evalNode.Eval(ctx) + if err != nil && !tc.err { + t.Fatal(err) + } + }) + } +}