From afb56c0e15707409a5b810874fda8ea5db545fb3 Mon Sep 17 00:00:00 2001 From: James Nugent Date: Wed, 13 Jul 2016 10:38:19 -0600 Subject: [PATCH] core: Add -json flag to `terraform output` This commit removes the ability to index into complex output types using `terraform output a_list 1` (for example), and adds a `-json` flag to the `terraform output` command, such that the output can be piped through a post-processor such as jq or json. This removes the need to allow arbitrary traversal of nested structures. It also adds tests of human readable ("normal") output with nested lists and maps, and of the new JSON output. --- command/output.go | 91 +++++++++++-------------------- command/output_test.go | 119 +++++++++++++++++++++++++++++++++++------ 2 files changed, 135 insertions(+), 75 deletions(-) diff --git a/command/output.go b/command/output.go index 23347a4dda1f..9054dfb4d3e2 100644 --- a/command/output.go +++ b/command/output.go @@ -2,10 +2,10 @@ package command import ( "bytes" + "encoding/json" "flag" "fmt" "sort" - "strconv" "strings" ) @@ -19,7 +19,10 @@ func (c *OutputCommand) Run(args []string) int { args = c.Meta.process(args, false) var module string + var jsonOutput bool + cmdFlags := flag.NewFlagSet("output", flag.ContinueOnError) + cmdFlags.BoolVar(&jsonOutput, "json", false, "json") cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path") cmdFlags.StringVar(&module, "module", "", "module") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } @@ -29,7 +32,7 @@ func (c *OutputCommand) Run(args []string) int { } args = cmdFlags.Args() - if len(args) > 2 { + if len(args) > 1 { 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") @@ -42,11 +45,6 @@ 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)) @@ -81,8 +79,18 @@ func (c *OutputCommand) Run(args []string) int { } if name == "" { - c.Ui.Output(outputsAsString(state, nil, false)) - return 0 + if jsonOutput { + jsonOutputs, err := json.MarshalIndent(mod.Outputs, "", " ") + if err != nil { + return 1 + } + + c.Ui.Output(string(jsonOutputs)) + return 0 + } else { + c.Ui.Output(outputsAsString(state, nil, false)) + return 0 + } } v, ok := mod.Outputs[name] @@ -95,66 +103,28 @@ func (c *OutputCommand) Run(args []string) int { return 1 } - switch output := v.Value.(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 jsonOutput { + jsonOutputs, err := json.MarshalIndent(v, "", " ") 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 + return 1 } - outputVal := output[indexInt] - switch typedOutputVal := outputVal.(type) { + c.Ui.Output(string(jsonOutputs)) + } else { + switch output := v.Value.(type) { case string: - c.Ui.Output(fmt.Sprintf("%s", typedOutputVal)) + c.Ui.Output(output) + return 0 case []interface{}: - c.Ui.Output(fmt.Sprintf("%s", formatNestedList("", typedOutputVal))) + c.Ui.Output(formatListOutput("", "", output)) + return 0 case map[string]interface{}: - c.Ui.Output(fmt.Sprintf("%s", formatNestedMap("", typedOutputVal))) - } - - return 0 - case map[string]interface{}: - if index == "" { c.Ui.Output(formatMapOutput("", "", output)) - break - } - - if value, ok := output[index]; ok { - switch typedOutputVal := value.(type) { - case string: - c.Ui.Output(fmt.Sprintf("%s", typedOutputVal)) - case []interface{}: - c.Ui.Output(fmt.Sprintf("%s", formatNestedList("", typedOutputVal))) - case map[string]interface{}: - c.Ui.Output(fmt.Sprintf("%s", formatNestedMap("", typedOutputVal))) - } return 0 - } else { + default: + c.Ui.Error(fmt.Sprintf("Unknown output type: %T", v.Type)) return 1 } - default: - c.Ui.Error(fmt.Sprintf("Unknown output type: %T", v.Type)) - return 1 } return 0 @@ -289,6 +259,9 @@ Options: -module=name If specified, returns the outputs for a specific module + -json If specified, machine readable output will be + printed in JSON format + ` return strings.TrimSpace(helpText) } diff --git a/command/output_test.go b/command/output_test.go index c553ff5aa459..1487d41cb0ac 100644 --- a/command/output_test.go +++ b/command/output_test.go @@ -14,10 +14,10 @@ import ( func TestOutput(t *testing.T) { originalState := &terraform.State{ Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ + { Path: []string{"root"}, Outputs: map[string]*terraform.OutputState{ - "foo": &terraform.OutputState{ + "foo": { Value: "bar", Type: "string", }, @@ -53,19 +53,19 @@ func TestOutput(t *testing.T) { func TestModuleOutput(t *testing.T) { originalState := &terraform.State{ Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ + { Path: []string{"root"}, Outputs: map[string]*terraform.OutputState{ - "foo": &terraform.OutputState{ + "foo": { Value: "bar", Type: "string", }, }, }, - &terraform.ModuleState{ + { Path: []string{"root", "my_module"}, Outputs: map[string]*terraform.OutputState{ - "blah": &terraform.OutputState{ + "blah": { Value: "tastatur", Type: "string", }, @@ -100,13 +100,100 @@ func TestModuleOutput(t *testing.T) { } } +func TestOutput_nestedListAndMap(t *testing.T) { + originalState := &terraform.State{ + Modules: []*terraform.ModuleState{ + { + Path: []string{"root"}, + Outputs: map[string]*terraform.OutputState{ + "foo": { + Value: []interface{}{ + map[string]interface{}{ + "key": "value", + "key2": "value2", + }, + map[string]interface{}{ + "key": "value", + }, + }, + Type: "list", + }, + }, + }, + }, + } + + statePath := testStateFile(t, originalState) + + ui := new(cli.MockUi) + c := &OutputCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{ + "-state", statePath, + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + + actual := strings.TrimSpace(ui.OutputWriter.String()) + expected := "foo = [\n {\n key = value,\n key2 = value2\n },\n {\n key = value\n }\n]" + if actual != expected { + t.Fatalf("bad:\n%#v\n%#v", expected, actual) + } +} + +func TestOutput_json(t *testing.T) { + originalState := &terraform.State{ + Modules: []*terraform.ModuleState{ + { + Path: []string{"root"}, + Outputs: map[string]*terraform.OutputState{ + "foo": { + Value: "bar", + Type: "string", + }, + }, + }, + }, + } + + statePath := testStateFile(t, originalState) + + ui := new(cli.MockUi) + c := &OutputCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{ + "-state", statePath, + "-json", + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + + actual := strings.TrimSpace(ui.OutputWriter.String()) + expected := "{\n \"foo\": {\n \"sensitive\": false,\n \"type\": \"string\",\n \"value\": \"bar\"\n }\n}" + if actual != expected { + t.Fatalf("bad:\n%#v\n%#v", expected, actual) + } +} + func TestMissingModuleOutput(t *testing.T) { originalState := &terraform.State{ Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ + { Path: []string{"root"}, Outputs: map[string]*terraform.OutputState{ - "foo": &terraform.OutputState{ + "foo": { Value: "bar", Type: "string", }, @@ -139,10 +226,10 @@ func TestMissingModuleOutput(t *testing.T) { func TestOutput_badVar(t *testing.T) { originalState := &terraform.State{ Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ + { Path: []string{"root"}, Outputs: map[string]*terraform.OutputState{ - "foo": &terraform.OutputState{ + "foo": { Value: "bar", Type: "string", }, @@ -173,14 +260,14 @@ func TestOutput_badVar(t *testing.T) { func TestOutput_blank(t *testing.T) { originalState := &terraform.State{ Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ + { Path: []string{"root"}, Outputs: map[string]*terraform.OutputState{ - "foo": &terraform.OutputState{ + "foo": { Value: "bar", Type: "string", }, - "name": &terraform.OutputState{ + "name": { Value: "john-doe", Type: "string", }, @@ -272,7 +359,7 @@ func TestOutput_noState(t *testing.T) { func TestOutput_noVars(t *testing.T) { originalState := &terraform.State{ Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ + { Path: []string{"root"}, Outputs: map[string]*terraform.OutputState{}, }, @@ -301,10 +388,10 @@ func TestOutput_noVars(t *testing.T) { func TestOutput_stateDefault(t *testing.T) { originalState := &terraform.State{ Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ + { Path: []string{"root"}, Outputs: map[string]*terraform.OutputState{ - "foo": &terraform.OutputState{ + "foo": { Value: "bar", Type: "string", },