From 9fc50a76c373d427c866d4502738ba5509a9168e Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 6 Oct 2016 19:40:04 -0400 Subject: [PATCH 1/2] Get rid of the list when parsing HCL maps for vars When we parse a map from HCL, it's decoded into a list of maps because HCL allows declaring a key multiple times to implicitly add it to a list. Since there's no terraform variable configuration that supports this structure, we can always flatten a []map[string]interface{} value to a map[string]interface{} and remove any type rrors trying to apply that value. --- command/flag_kv.go | 36 ++++++++++++++++++++++++++++++++---- command/flag_kv_test.go | 25 +++++++++++++++++++------ 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/command/flag_kv.go b/command/flag_kv.go index f93eb8b81..f58b51c05 100644 --- a/command/flag_kv.go +++ b/command/flag_kv.go @@ -111,6 +111,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 } @@ -185,10 +189,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 7144ca8e8..df71461e6 100644 --- a/command/flag_kv_test.go +++ b/command/flag_kv_test.go @@ -2,10 +2,11 @@ package command import ( "flag" - "github.com/davecgh/go-spew/spew" "io/ioutil" "reflect" "testing" + + "github.com/davecgh/go-spew/spew" ) func TestFlagStringKV_impl(t *testing.T) { @@ -118,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, @@ -169,6 +168,10 @@ func TestFlagKVFile(t *testing.T) { inputLibucl := ` foo = "bar" ` + inputMap := ` +foo = { + k = "v" +}` inputJson := `{ "foo": "bar"}` @@ -195,6 +198,16 @@ foo = "bar" map[string]interface{}{"map.key": "foo"}, false, }, + + { + inputMap, + map[string]interface{}{ + "foo": map[string]interface{}{ + "k": "v", + }, + }, + false, + }, } path := testTempFile(t) From 48c8afaa11bc77f4f7965a92d513631b0019bc61 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Fri, 7 Oct 2016 15:00:50 -0400 Subject: [PATCH 2/2] Check for multi-values maps in output too A map value read from a config file will be the default `[]map[string]interface{}` type decoded from HCL. Since this type can't be applied to a variable, it's likely that it was a simple map. If there's a single map value, we can pull that out of the slice during Eval. --- terraform/eval_output.go | 13 ++++++++ terraform/eval_output_test.go | 56 +++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 terraform/eval_output_test.go diff --git a/terraform/eval_output.go b/terraform/eval_output.go index bee4f1084..cf61781e5 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 000000000..f73b127de --- /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) + } + }) + } +}