diff --git a/command/output.go b/command/output.go index 23347a4dda..9054dfb4d3 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 c553ff5aa4..1487d41cb0 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 f1a70394ef..b284c79a56 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