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.
This commit is contained in:
James Nugent 2016-07-13 10:38:19 -06:00
parent ef3aad1231
commit b4048dfc1d
3 changed files with 145 additions and 82 deletions

View File

@ -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)
}

View File

@ -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",
},

View File

@ -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