From b4048dfc1da2a36963850d6357e6921a340eccea 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 | 105 ++++++---------- command/output_test.go | 119 +++++++++++++++--- .../source/docs/commands/output.html.markdown | 3 + 3 files changed, 145 insertions(+), 82 deletions(-) diff --git a/command/output.go b/command/output.go index 23347a4dd..9054dfb4d 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 - } - - outputVal := output[indexInt] - switch typedOutputVal := outputVal.(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 - 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 { return 1 } - default: - c.Ui.Error(fmt.Sprintf("Unknown output type: %T", v.Type)) - return 1 + + c.Ui.Output(string(jsonOutputs)) + } else { + switch output := v.Value.(type) { + case string: + c.Ui.Output(output) + return 0 + case []interface{}: + c.Ui.Output(formatListOutput("", "", output)) + return 0 + case map[string]interface{}: + c.Ui.Output(formatMapOutput("", "", output)) + return 0 + 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 c553ff5aa..1487d41cb 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", }, diff --git a/website/source/docs/commands/output.html.markdown b/website/source/docs/commands/output.html.markdown index f1a70394e..b284c79a5 100644 --- a/website/source/docs/commands/output.html.markdown +++ b/website/source/docs/commands/output.html.markdown @@ -20,6 +20,9 @@ current directory for the state file to query. The command-line flags are all optional. The list of available flags are: +* `-json` - If specified, the outputs are formatted as a JSON object, with + a key per output. This can be piped into tools such as `jq` for further + processing. * `-state=path` - Path to the state file. Defaults to "terraform.tfstate". * `-module=module_name` - The module path which has needed output. By default this is the root path. Other modules can be specified by