Skip to content

Commit

Permalink
command: Add remote state capability to output subcommand
Browse files Browse the repository at this point in the history
Add the ability to get *separate* remote state via the
"terraform output" subcommand. Syntax follows
"terraform remote config" with some slight deviations.

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 added a little note about the usage of -module, as it is ignored
unless you are actually specifying an output to display, versus all
outputs. This is due to limitations of command.outputsAsString() and was
present before this work.
  • Loading branch information
Chris Marchesi committed May 13, 2016
1 parent 2bca697 commit ee8bd6e
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 17 deletions.
67 changes: 52 additions & 15 deletions command/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import (
"sort"
"strconv"
"strings"

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

// OutputCommand is a Command implementation that reads an output
Expand All @@ -18,10 +21,15 @@ 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 {
Expand All @@ -47,12 +55,6 @@ func (c *OutputCommand) Run(args []string) int {
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 {
Expand All @@ -62,7 +64,29 @@ func (c *OutputCommand) Run(args []string) int {
// Get the proper module we want to get outputs for
modPath := strings.Split(module, ".")

state := stateStore.State()
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 {
Expand Down Expand Up @@ -194,19 +218,32 @@ 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".
-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.
-no-color If specified, output won't contain any color.
-remote-config="k=v" Specifies configuration for the remote storage
backend. This can be specified multiple times.
-module=name If specified, returns the outputs for a
specific module
-no-color If specified, output won't contain any color.
-module=name If specified, returns the outputs for a
specific module. Only valid when a specific output
is provided.
`
return strings.TrimSpace(helpText)
}
Expand Down
82 changes: 82 additions & 0 deletions command/output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ package command

import (
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"testing"

Expand Down Expand Up @@ -337,3 +341,81 @@ func TestOutput_stateDefault(t *testing.T) {
t.Fatalf("bad: %#v", actual)
}
}

// remoteStateResponseText defines mock remote state response text.
const remoteStateResponseText = `
{
"version": 1,
"serial": 1,
"remote": {
"type": "http",
"config": {
"address": "http://127.0.0.1:12345/",
"skip_cert_verification": "0"
}
},
"modules": [{
"path": [
"root"
],
"outputs": {
"foo": "bar",
"baz": "qux"
},
"resources": {}
}]
}
`

// remoteStateOutputExpected defines the expected outputs.
const remoteStateOutputExpected = "foo = bar\nbaz = qux\n"

// newRemoteStateHTTPTestServer retuns a HTTP test server.
func newRemoteStateHTTPTestServer(f func(w http.ResponseWriter, r *http.Request)) *httptest.Server {
ts := httptest.NewServer(http.HandlerFunc(f))
return ts
}

// httpRemoteStateTestServer returns a fully configured HTTP test server for
// HTTP remote state.
func httpRemoteStateTestServer() *httptest.Server {
return newRemoteStateHTTPTestServer(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
http.Error(w, remoteStateResponseText, http.StatusOK)
})
}

// TestOutput_remoteState tests remote state outputs.
func TestOutput_remoteState(t *testing.T) {
ts := httpRemoteStateTestServer()
defer ts.Close()

ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}

args := []string{
"-remote=true",
"-remote-backend=http",
"-remote-config=address=" + ts.URL,
"",
}

if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}

// Our output needs to be sorted here, remote state returns unordered.
expectedOutput := strings.Split("\n", remoteStateOutputExpected)
sort.Strings(expectedOutput)

output := strings.Split("\n", ui.OutputWriter.String())
sort.Strings(output)
if reflect.DeepEqual(output, expectedOutput) != true {
t.Fatalf("Expected output: %#v\ngiven: %#v", expectedOutput, output)
}
}
16 changes: 14 additions & 2 deletions website/source/docs/commands/output.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ description: |-
# Command: output

The `terraform output` command is used to extract the value of
an output variable from the state file.
an output variable from the state file, or from a Terraform
remote state.

## Usage

Expand All @@ -21,8 +22,19 @@ current directory for the state file to query.
The command-line flags are all optional. The list of available flags are:

* `-state=path` - Path to the state file. 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`][1] for a list of supported backends.
* `-remote-config="k=v"` Specifies configuration for the remote storage
backend. This can be specified multiple times.
* `-module=module_name` - The module path which has needed output.
By default this is the root path. Other modules can be specified by
a period-separated list. Example: "foo" would reference the module
"foo" but "foo.bar" would reference the "bar" module in the "foo"
module.
module. Note that this switch is only valid when an output is provided
on the command line.

[1]: /docs/commands/remote-config.html

0 comments on commit ee8bd6e

Please sign in to comment.