Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WIP: Moving remote state output support to "terraform remote output"
Browse files Browse the repository at this point in the history
Chris Marchesi committed May 16, 2016
1 parent ee8bd6e commit 1af38bb
Showing 8 changed files with 837 additions and 324 deletions.
60 changes: 2 additions & 58 deletions command/apply.go
Original file line number Diff line number Diff line change
@@ -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"
)

@@ -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))
}
}
@@ -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())
}
198 changes: 17 additions & 181 deletions command/output.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
package command

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

"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/terraform"
)

// OutputCommand is a Command implementation that reads an output
@@ -36,182 +30,37 @@ func (c *OutputCommand) Run(args []string) int {
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]
}

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

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

var state *terraform.State
if remoteState == true {
client, err := remote.NewClient(remoteBackend, backendConfig)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error reading remote state: %s", err))
return 1
}

remoteState := &remote.State{Client: client}
if err := remoteState.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf("Error reading remote state: %s", err))
return 1
}
state = remoteState.State()
} else {

stateStore, err := c.Meta.State()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error reading state: %s", err))
return 1
}
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))
stateStore, err := c.Meta.State()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error reading state: %s", err))
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."))
mod, err := moduleFromState(stateStore.State(), module)
if err != nil {
c.Ui.Error(err.Error())
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."))
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 {
@@ -226,19 +75,6 @@ Options:
-state=path Path to the state file to read. Defaults to
"terraform.tfstate".
-remote=false Allows the fetching of outputs from remote state,
independent of any existing Terraform configuration.
If this is specified, -path is ignored, and remote
state path should be specified via -remote-config
as required.
-remote-backend=Atlas Specifies the type of remote backend. See
"terraform remote config -help" for a list of
supported backends. Defaults to Atlas.
-remote-config="k=v" Specifies configuration for the remote storage
backend. This can be specified multiple times.
-no-color If specified, output won't contain any color.
-module=name If specified, returns the outputs for a
Loading

0 comments on commit 1af38bb

Please sign in to comment.