Skip to content

Commit

Permalink
command: Add "terraform remote output", allow all outputs w/modules
Browse files Browse the repository at this point in the history
Add the ability to get *separate* remote state via the
"terraform remote output" subcommand. Syntax follows
"terraform remote config", with a couple of additions.

This allows the ability to extract remote state from Terraform in
places were remote state may not be present, without having to
pre-configure remote state.

Also, as part of this work, there has been a re-factoring of the
outputsAsString function to allow both "terraform output" and
"terraform remote output" to print all outputs, even when an optional
module is supplied.
  • Loading branch information
Chris Marchesi committed May 16, 2016
1 parent 2bca697 commit 7d60d46
Show file tree
Hide file tree
Showing 12 changed files with 1,001 additions and 219 deletions.
60 changes: 2 additions & 58 deletions command/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@ import (
"bytes"
"fmt"
"os"
"sort"
"strings"

"github.com/hashicorp/go-getter"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/terraform"
)

Expand Down Expand Up @@ -250,8 +248,8 @@ func (c *ApplyCommand) Run(args []string) int {
c.Meta.StateOutPath())))
}

if !c.Destroy {
if outputs := outputsAsString(state, ctx.Module().Config().Outputs, true); outputs != "" {
if !c.Destroy && state != nil {
if outputs := allOutputsAsString(state.RootModule(), ctx.Module().Config().Outputs, true); outputs != "" {
c.Ui.Output(c.Colorize().Color(outputs))
}
}
Expand Down Expand Up @@ -376,57 +374,3 @@ Options:
`
return strings.TrimSpace(helpText)
}

func outputsAsString(state *terraform.State, schema []*config.Output, includeHeader bool) string {
if state == nil {
return ""
}

outputs := state.RootModule().Outputs
outputBuf := new(bytes.Buffer)
if len(outputs) > 0 {
schemaMap := make(map[string]*config.Output)
if schema != nil {
for _, s := range schema {
schemaMap[s.Name] = s
}
}

if includeHeader {
outputBuf.WriteString("[reset][bold][green]\nOutputs:\n\n")
}

// Output the outputs in alphabetical order
keyLen := 0
ks := make([]string, 0, len(outputs))
for key, _ := range outputs {
ks = append(ks, key)
if len(key) > keyLen {
keyLen = len(key)
}
}
sort.Strings(ks)

for _, k := range ks {
schema, ok := schemaMap[k]
if ok && schema.Sensitive {
outputBuf.WriteString(fmt.Sprintf("%s = <sensitive>\n", k))
continue
}

v := outputs[k]
switch typedV := v.(type) {
case string:
outputBuf.WriteString(fmt.Sprintf("%s = %s\n", k, typedV))
case []interface{}:
outputBuf.WriteString(formatListOutput("", k, typedV))
outputBuf.WriteString("\n")
case map[string]interface{}:
outputBuf.WriteString(formatMapOutput("", k, typedV))
outputBuf.WriteString("\n")
}
}
}

return strings.TrimSpace(outputBuf.String())
}
181 changes: 27 additions & 154 deletions command/output.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package command

import (
"bytes"
"flag"
"fmt"
"sort"
"strconv"
"strings"
)

Expand All @@ -18,194 +15,70 @@ type OutputCommand struct {
func (c *OutputCommand) Run(args []string) int {
args = c.Meta.process(args, false)

var module string
var module, remoteBackend string
var remoteState bool
var backendConfig map[string]string
cmdFlags := flag.NewFlagSet("output", flag.ContinueOnError)
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
cmdFlags.StringVar(&module, "module", "", "module")
cmdFlags.BoolVar(&remoteState, "remote", false, "remote")
cmdFlags.StringVar(&remoteBackend, "remote-backend", "atlas", "remote-backend")
cmdFlags.Var((*FlagKV)(&backendConfig), "remote-config", "remote-config")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }

if err := cmdFlags.Parse(args); err != nil {
return 1
}

args = cmdFlags.Args()
if len(args) > 2 {
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")
cmdFlags.Usage()
name, index, err := parseOutputNameIndex(cmdFlags.Args())
if err != nil {
c.Ui.Error(err.Error())
return 1
}

name := ""
if len(args) > 0 {
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))
return 1
}

if module == "" {
module = "root"
} else {
module = "root." + module
}

// Get the proper module we want to get outputs for
modPath := strings.Split(module, ".")

state := stateStore.State()
mod := state.ModuleByPath(modPath)

if mod == nil {
c.Ui.Error(fmt.Sprintf(
"The module %s could not be found. There is nothing to output.",
module))
return 1
}

if state.Empty() || len(mod.Outputs) == 0 {
c.Ui.Error(fmt.Sprintf(
"The state file has no outputs defined. Define an output\n" +
"in your configuration with the `output` directive and re-run\n" +
"`terraform apply` for it to become available."))
return 1
}

if name == "" {
c.Ui.Output(outputsAsString(state, nil, false))
return 0
}

v, ok := mod.Outputs[name]
if !ok {
c.Ui.Error(fmt.Sprintf(
"The output variable requested could not be found in the state\n" +
"file. If you recently added this to your configuration, be\n" +
"sure to run `terraform apply`, since the state won't be updated\n" +
"with new output variables until that command is run."))
mod, err := moduleFromState(stateStore.State(), module)
if err != nil {
c.Ui.Error(err.Error())
return 1
}

switch output := v.(type) {
case string:
c.Ui.Output(output)
return 0
case []interface{}:
if index == "" {
c.Ui.Output(formatListOutput("", "", output))
break
}

indexInt, err := strconv.Atoi(index)
var out string
if name != "" {
out, err = singleOutputAsString(mod, name, 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
case map[string]interface{}:
if index == "" {
c.Ui.Output(formatMapOutput("", "", output))
break
}

if value, ok := output[index]; ok {
c.Ui.Output(fmt.Sprintf("%s", value))
return 0
} else {
c.Ui.Error(err.Error())
return 1
}
default:
panic(fmt.Errorf("Unknown output type: %T", output))
}

return 0
}

func formatListOutput(indent, outputName string, outputList []interface{}) string {
keyIndent := ""

outputBuf := new(bytes.Buffer)
if outputName != "" {
outputBuf.WriteString(fmt.Sprintf("%s%s = [", indent, outputName))
keyIndent = " "
}

for _, value := range outputList {
outputBuf.WriteString(fmt.Sprintf("\n%s%s%s", indent, keyIndent, value))
}

if outputName != "" {
outputBuf.WriteString(fmt.Sprintf("\n%s]", indent))
}

return strings.TrimPrefix(outputBuf.String(), "\n")
}

func formatMapOutput(indent, outputName string, outputMap map[string]interface{}) string {
ks := make([]string, 0, len(outputMap))
for k, _ := range outputMap {
ks = append(ks, k)
}
sort.Strings(ks)

keyIndent := ""

outputBuf := new(bytes.Buffer)
if outputName != "" {
outputBuf.WriteString(fmt.Sprintf("%s%s = {", indent, outputName))
keyIndent = " "
}

for _, k := range ks {
v := outputMap[k]
outputBuf.WriteString(fmt.Sprintf("\n%s%s%s = %v", indent, keyIndent, k, v))
} else {
out = allOutputsAsString(mod, nil, false)
}

if outputName != "" {
outputBuf.WriteString(fmt.Sprintf("\n%s}", indent))
}
c.Ui.Output(out)

return strings.TrimPrefix(outputBuf.String(), "\n")
return 0
}

func (c *OutputCommand) Help() string {
helpText := `
Usage: terraform output [options] [NAME]
Reads an output variable from a Terraform state file and prints
the value. If NAME is not specified, all outputs are printed.
Reads an output variable from a Terraform state file, or remote state,
and prints the value. If NAME is not specified, all outputs are printed.
Options:
-state=path Path to the state file to read. Defaults to
"terraform.tfstate".
-state=path Path to the state file to read. Defaults to
"terraform.tfstate".
-no-color If specified, output won't contain any color.
-no-color If specified, output won't contain any color.
-module=name If specified, returns the outputs for a
specific module
-module=name If specified, returns the outputs for a
specific module.
`
return strings.TrimSpace(helpText)
Expand Down
Loading

0 comments on commit 7d60d46

Please sign in to comment.