Merge pull request #7608 from hashicorp/nested-output-formatting
core: Correctly format nested outputs
This commit is contained in:
commit
9ca3f2dd13
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue