Merge pull request #7608 from hashicorp/nested-output-formatting

core: Correctly format nested outputs
This commit is contained in:
James Nugent 2016-07-13 12:14:47 -06:00 committed by GitHub
commit 9ca3f2dd13
3 changed files with 204 additions and 69 deletions

View File

@ -2,10 +2,10 @@ package command
import ( import (
"bytes" "bytes"
"encoding/json"
"flag" "flag"
"fmt" "fmt"
"sort" "sort"
"strconv"
"strings" "strings"
) )
@ -19,7 +19,10 @@ func (c *OutputCommand) Run(args []string) int {
args = c.Meta.process(args, false) args = c.Meta.process(args, false)
var module string var module string
var jsonOutput bool
cmdFlags := flag.NewFlagSet("output", flag.ContinueOnError) cmdFlags := flag.NewFlagSet("output", flag.ContinueOnError)
cmdFlags.BoolVar(&jsonOutput, "json", false, "json")
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path") cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
cmdFlags.StringVar(&module, "module", "", "module") cmdFlags.StringVar(&module, "module", "", "module")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
@ -29,7 +32,7 @@ func (c *OutputCommand) Run(args []string) int {
} }
args = cmdFlags.Args() args = cmdFlags.Args()
if len(args) > 2 { if len(args) > 1 {
c.Ui.Error( c.Ui.Error(
"The output command expects exactly one argument with the name\n" + "The output command expects exactly one argument with the name\n" +
"of an output variable or no arguments to show all outputs.\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] name = args[0]
} }
index := ""
if len(args) > 1 {
index = args[1]
}
stateStore, err := c.Meta.State() stateStore, err := c.Meta.State()
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf("Error reading state: %s", err)) c.Ui.Error(fmt.Sprintf("Error reading state: %s", err))
@ -81,9 +79,19 @@ func (c *OutputCommand) Run(args []string) int {
} }
if name == "" { if name == "" {
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)) c.Ui.Output(outputsAsString(state, nil, false))
return 0 return 0
} }
}
v, ok := mod.Outputs[name] v, ok := mod.Outputs[name]
if !ok { if !ok {
@ -95,55 +103,50 @@ func (c *OutputCommand) Run(args []string) int {
return 1 return 1
} }
if jsonOutput {
jsonOutputs, err := json.MarshalIndent(v, "", " ")
if err != nil {
return 1
}
c.Ui.Output(string(jsonOutputs))
} else {
switch output := v.Value.(type) { switch output := v.Value.(type) {
case string: case string:
c.Ui.Output(output) c.Ui.Output(output)
return 0 return 0
case []interface{}: case []interface{}:
if index == "" {
c.Ui.Output(formatListOutput("", "", output)) c.Ui.Output(formatListOutput("", "", output))
break
}
indexInt, err := strconv.Atoi(index)
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
}
c.Ui.Output(fmt.Sprintf("%s", output[indexInt]))
return 0 return 0
case map[string]interface{}: case map[string]interface{}:
if index == "" {
c.Ui.Output(formatMapOutput("", "", output)) c.Ui.Output(formatMapOutput("", "", output))
break
}
if value, ok := output[index]; ok {
c.Ui.Output(fmt.Sprintf("%s", value))
return 0 return 0
} else {
return 1
}
default: default:
c.Ui.Error(fmt.Sprintf("Unknown output type: %T", v.Type)) c.Ui.Error(fmt.Sprintf("Unknown output type: %T", v.Type))
return 1 return 1
} }
}
return 0 return 0
} }
func formatNestedList(indent string, outputList []interface{}) string {
outputBuf := new(bytes.Buffer)
outputBuf.WriteString(fmt.Sprintf("%s[", indent))
lastIdx := len(outputList) - 1
for i, value := range outputList {
outputBuf.WriteString(fmt.Sprintf("\n%s%s%s", indent, " ", value))
if i != lastIdx {
outputBuf.WriteString(",")
}
}
outputBuf.WriteString(fmt.Sprintf("\n%s]", indent))
return strings.TrimPrefix(outputBuf.String(), "\n")
}
func formatListOutput(indent, outputName string, outputList []interface{}) string { func formatListOutput(indent, outputName string, outputList []interface{}) string {
keyIndent := "" keyIndent := ""
@ -154,8 +157,23 @@ func formatListOutput(indent, outputName string, outputList []interface{}) strin
keyIndent = " " keyIndent = " "
} }
for _, value := range outputList { lastIdx := len(outputList) - 1
for i, value := range outputList {
switch typedValue := value.(type) {
case string:
outputBuf.WriteString(fmt.Sprintf("\n%s%s%s", indent, keyIndent, value)) outputBuf.WriteString(fmt.Sprintf("\n%s%s%s", indent, keyIndent, value))
case []interface{}:
outputBuf.WriteString(fmt.Sprintf("\n%s%s", indent,
formatNestedList(indent+keyIndent, typedValue)))
case map[string]interface{}:
outputBuf.WriteString(fmt.Sprintf("\n%s%s", indent,
formatNestedMap(indent+keyIndent, typedValue)))
}
if lastIdx != i {
outputBuf.WriteString(",")
}
} }
if outputName != "" { if outputName != "" {
@ -169,6 +187,30 @@ func formatListOutput(indent, outputName string, outputList []interface{}) strin
return strings.TrimPrefix(outputBuf.String(), "\n") return strings.TrimPrefix(outputBuf.String(), "\n")
} }
func formatNestedMap(indent string, outputMap map[string]interface{}) string {
ks := make([]string, 0, len(outputMap))
for k, _ := range outputMap {
ks = append(ks, k)
}
sort.Strings(ks)
outputBuf := new(bytes.Buffer)
outputBuf.WriteString(fmt.Sprintf("%s{", indent))
lastIdx := len(outputMap) - 1
for i, k := range ks {
v := outputMap[k]
outputBuf.WriteString(fmt.Sprintf("\n%s%s = %v", indent+" ", k, v))
if lastIdx != i {
outputBuf.WriteString(",")
}
}
outputBuf.WriteString(fmt.Sprintf("\n%s}", indent))
return strings.TrimPrefix(outputBuf.String(), "\n")
}
func formatMapOutput(indent, outputName string, outputMap map[string]interface{}) string { func formatMapOutput(indent, outputName string, outputMap map[string]interface{}) string {
ks := make([]string, 0, len(outputMap)) ks := make([]string, 0, len(outputMap))
for k, _ := range outputMap { for k, _ := range outputMap {
@ -217,6 +259,9 @@ Options:
-module=name If specified, returns the outputs for a -module=name If specified, returns the outputs for a
specific module specific module
-json If specified, machine readable output will be
printed in JSON format
` `
return strings.TrimSpace(helpText) return strings.TrimSpace(helpText)
} }

View File

@ -14,10 +14,10 @@ import (
func TestOutput(t *testing.T) { func TestOutput(t *testing.T) {
originalState := &terraform.State{ originalState := &terraform.State{
Modules: []*terraform.ModuleState{ Modules: []*terraform.ModuleState{
&terraform.ModuleState{ {
Path: []string{"root"}, Path: []string{"root"},
Outputs: map[string]*terraform.OutputState{ Outputs: map[string]*terraform.OutputState{
"foo": &terraform.OutputState{ "foo": {
Value: "bar", Value: "bar",
Type: "string", Type: "string",
}, },
@ -53,19 +53,19 @@ func TestOutput(t *testing.T) {
func TestModuleOutput(t *testing.T) { func TestModuleOutput(t *testing.T) {
originalState := &terraform.State{ originalState := &terraform.State{
Modules: []*terraform.ModuleState{ Modules: []*terraform.ModuleState{
&terraform.ModuleState{ {
Path: []string{"root"}, Path: []string{"root"},
Outputs: map[string]*terraform.OutputState{ Outputs: map[string]*terraform.OutputState{
"foo": &terraform.OutputState{ "foo": {
Value: "bar", Value: "bar",
Type: "string", Type: "string",
}, },
}, },
}, },
&terraform.ModuleState{ {
Path: []string{"root", "my_module"}, Path: []string{"root", "my_module"},
Outputs: map[string]*terraform.OutputState{ Outputs: map[string]*terraform.OutputState{
"blah": &terraform.OutputState{ "blah": {
Value: "tastatur", Value: "tastatur",
Type: "string", 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) { func TestMissingModuleOutput(t *testing.T) {
originalState := &terraform.State{ originalState := &terraform.State{
Modules: []*terraform.ModuleState{ Modules: []*terraform.ModuleState{
&terraform.ModuleState{ {
Path: []string{"root"}, Path: []string{"root"},
Outputs: map[string]*terraform.OutputState{ Outputs: map[string]*terraform.OutputState{
"foo": &terraform.OutputState{ "foo": {
Value: "bar", Value: "bar",
Type: "string", Type: "string",
}, },
@ -139,10 +226,10 @@ func TestMissingModuleOutput(t *testing.T) {
func TestOutput_badVar(t *testing.T) { func TestOutput_badVar(t *testing.T) {
originalState := &terraform.State{ originalState := &terraform.State{
Modules: []*terraform.ModuleState{ Modules: []*terraform.ModuleState{
&terraform.ModuleState{ {
Path: []string{"root"}, Path: []string{"root"},
Outputs: map[string]*terraform.OutputState{ Outputs: map[string]*terraform.OutputState{
"foo": &terraform.OutputState{ "foo": {
Value: "bar", Value: "bar",
Type: "string", Type: "string",
}, },
@ -173,14 +260,14 @@ func TestOutput_badVar(t *testing.T) {
func TestOutput_blank(t *testing.T) { func TestOutput_blank(t *testing.T) {
originalState := &terraform.State{ originalState := &terraform.State{
Modules: []*terraform.ModuleState{ Modules: []*terraform.ModuleState{
&terraform.ModuleState{ {
Path: []string{"root"}, Path: []string{"root"},
Outputs: map[string]*terraform.OutputState{ Outputs: map[string]*terraform.OutputState{
"foo": &terraform.OutputState{ "foo": {
Value: "bar", Value: "bar",
Type: "string", Type: "string",
}, },
"name": &terraform.OutputState{ "name": {
Value: "john-doe", Value: "john-doe",
Type: "string", Type: "string",
}, },
@ -272,7 +359,7 @@ func TestOutput_noState(t *testing.T) {
func TestOutput_noVars(t *testing.T) { func TestOutput_noVars(t *testing.T) {
originalState := &terraform.State{ originalState := &terraform.State{
Modules: []*terraform.ModuleState{ Modules: []*terraform.ModuleState{
&terraform.ModuleState{ {
Path: []string{"root"}, Path: []string{"root"},
Outputs: map[string]*terraform.OutputState{}, Outputs: map[string]*terraform.OutputState{},
}, },
@ -301,10 +388,10 @@ func TestOutput_noVars(t *testing.T) {
func TestOutput_stateDefault(t *testing.T) { func TestOutput_stateDefault(t *testing.T) {
originalState := &terraform.State{ originalState := &terraform.State{
Modules: []*terraform.ModuleState{ Modules: []*terraform.ModuleState{
&terraform.ModuleState{ {
Path: []string{"root"}, Path: []string{"root"},
Outputs: map[string]*terraform.OutputState{ Outputs: map[string]*terraform.OutputState{
"foo": &terraform.OutputState{ "foo": {
Value: "bar", Value: "bar",
Type: "string", 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: 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". * `-state=path` - Path to the state file. Defaults to "terraform.tfstate".
* `-module=module_name` - The module path which has needed output. * `-module=module_name` - The module path which has needed output.
By default this is the root path. Other modules can be specified by By default this is the root path. Other modules can be specified by