Skip to content

Commit

Permalink
core: Add -json flag to terraform output
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jen20 committed Jul 13, 2016
1 parent ef3aad1 commit afb56c0
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 75 deletions.
91 changes: 32 additions & 59 deletions command/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package command

import (
"bytes"
"encoding/json"
"flag"
"fmt"
"sort"
"strconv"
"strings"
)

Expand All @@ -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()) }
Expand All @@ -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")
Expand All @@ -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))
Expand Down Expand Up @@ -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]
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
119 changes: 103 additions & 16 deletions command/output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
Expand Down Expand Up @@ -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",
},
Expand Down Expand Up @@ -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",
},
Expand Down Expand Up @@ -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",
},
Expand Down Expand Up @@ -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",
},
Expand Down Expand Up @@ -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{},
},
Expand Down Expand Up @@ -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",
},
Expand Down

0 comments on commit afb56c0

Please sign in to comment.