Skip to content

Commit

Permalink
terraform show -format=json
Browse files Browse the repository at this point in the history
As a simple, non-intrusive way to get machine-readable plan output, add
a -format=json argument to the "terraform show" command. This then allows
external tools to interrogate plan files, e.g. to enforce additional
restrictions on what can be changed that Terraform itself is unable to
represent.

For completeness this also supports JSON-formatted state output, but of
course that is rather less interesting because the on-disk state file is
already in this same format.
  • Loading branch information
apparentlymart committed Dec 11, 2016
1 parent d3b88b9 commit dda157e
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 10 deletions.
19 changes: 19 additions & 0 deletions command/format_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package command

import (
"bytes"
"encoding/json"
"fmt"
"sort"
"strings"
Expand Down Expand Up @@ -229,3 +230,21 @@ func formatPlanModuleSingle(
len(m.Resources)))
buf.WriteString(opts.Color.Color("[reset]\n"))
}

func FormatPlanJSON(plan *terraform.Plan) string {
err := plan.State.PrepareForWrite()
if err != nil {
// should never happen
panic(err)
}

dst := bytes.NewBuffer(make([]byte, 0, 64))
enc := json.NewEncoder(dst)
enc.SetIndent("", " ")
err = enc.Encode(plan)
if err != nil {
// should never happen
panic(err)
}
return dst.String()
}
13 changes: 13 additions & 0 deletions command/format_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package command

import (
"bytes"
"encoding/json"
"fmt"
"sort"
"strings"
Expand Down Expand Up @@ -150,3 +151,15 @@ func formatStateModuleSingle(
// Now just write how many resources are in here.
buf.WriteString(fmt.Sprintf(" %d resource(s)\n", len(m.Resources)))
}

func FormatStateJSON(state *terraform.State) string {
dst := bytes.NewBuffer(make([]byte, 0, 64))
enc := json.NewEncoder(dst)
enc.SetIndent("", " ")
err := enc.Encode(state)
if err != nil {
// should never happen
panic(err)
}
return dst.String()
}
52 changes: 42 additions & 10 deletions command/show.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ func (c *ShowCommand) Run(args []string) int {

args = c.Meta.process(args, false)

var format string

cmdFlags := flag.NewFlagSet("show", flag.ContinueOnError)
c.addModuleDepthFlag(cmdFlags, &moduleDepth)
cmdFlags.StringVar(&format, "format", "ui", "format-mode")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
Expand Down Expand Up @@ -91,21 +94,37 @@ func (c *ShowCommand) Run(args []string) int {
return 1
}

if plan != nil {
c.Ui.Output(FormatPlan(&FormatPlanOpts{
Plan: plan,
switch format {
case "ui": // the default, if no -format option is specified
if plan != nil {
c.Ui.Output(FormatPlan(&FormatPlanOpts{
Plan: plan,
Color: c.Colorize(),
ModuleDepth: moduleDepth,
}))
return 0
}

c.Ui.Output(FormatState(&FormatStateOpts{
State: state,
Color: c.Colorize(),
ModuleDepth: moduleDepth,
}))
return 0
}

c.Ui.Output(FormatState(&FormatStateOpts{
State: state,
Color: c.Colorize(),
ModuleDepth: moduleDepth,
}))
return 0
case "json":
if plan != nil {
c.Ui.Output(FormatPlanJSON(plan))
return 0
}

c.Ui.Output(FormatStateJSON(state))
return 0

default:
c.Ui.Error(fmt.Sprintf("%q is not a supported output format", format))
return 1
}
}

func (c *ShowCommand) Help() string {
Expand All @@ -117,11 +136,24 @@ Usage: terraform show [options] [path]
Options:
-format=name Specifies the output format. By default, human-readable
output is produced. Set -format=json for a
machine-readable JSON data structure. The remaining
options are ignored for JSON output.
-module-depth=n Specifies the depth of modules to show in the output.
By default this is -1, which will expand all.
-no-color If specified, output won't contain any color.
WARNING: JSON output is provided as a convenience for lightweight integrations
with external tools, but the JSON format is *not* frozen and may change in
future versions of Terraform.
JSON output is also more detailed than the standard human-readable output and
may contain sensitive information that is not normally included, including
the values of any outputs that are marked as sensitive.
`
return strings.TrimSpace(helpText)
}
Expand Down
73 changes: 73 additions & 0 deletions command/show_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package command

import (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
Expand Down Expand Up @@ -183,3 +184,75 @@ func TestShow_state(t *testing.T) {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
}

func TestShow_planJSON(t *testing.T) {
planPath := testPlanFile(t, &terraform.Plan{
Module: new(module.Tree),
State: testState(),
})

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

args := []string{
"-format=json",
planPath,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}

output := ui.OutputWriter.Bytes()
data := make(map[string]interface{})
err := json.Unmarshal(output, &data)
if err != nil {
t.Fatalf("not valid JSON: %s", err)
}
// Sniff to make sure this looks like a plan rather than a state
if _, ok := data["module"]; !ok {
t.Fatalf("JSON does not contain 'module' key")
}
if _, ok := data["state"]; !ok {
t.Fatalf("JSON does not contain 'state' key")
}
}

func TestShow_stateJSON(t *testing.T) {
originalState := testState()
statePath := testStateFile(t, originalState)

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

args := []string{
"-format=json",
statePath,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}

output := ui.OutputWriter.Bytes()
data := make(map[string]interface{})
err := json.Unmarshal(output, &data)
if err != nil {
t.Fatalf("not valid JSON: %s", err)
}
// Sniff to make sure this looks like a state rather than a plan
if _, ok := data["modules"]; !ok {
t.Fatalf("JSON does not contain 'modules' key")
}
if _, ok := data["lineage"]; !ok {
t.Fatalf("JSON does not contain 'lineage' key")
}
}
19 changes: 19 additions & 0 deletions website/source/docs/commands/show.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,27 @@ file. If no path is specified, the current state will be shown.

The command-line flags are all optional. The list of available flags are:

* `-format=name` - Produces output in an alternative format. Currently "json"
is the only supported alternative format. See below for more information
and caveats regarding the JSON output.

* `-module-depth=n` - Specifies the depth of modules to show in the output.
By default this is -1, which will expand all.

* `-no-color` - Disables output with coloring

## JSON Output

As a convenience for intepreting Terraform data using external tools, Terraform
can produce detailed plan and state information in JSON format.

However, since Terraform is still quickly evolving we are unable to guarantee
100% compatibility with the current JSON data structures in future versions,
and so the current data structures are not documented in detail.

Please use this feature sparingly and with care, and be ready to update any
integrations when moving to newer versions of Terraform.

~> **Warning** The JSON output is generally more detailed than the
human-readable output, and in particular can include *sensitive information*.
The JSON output must therefore be stored and transmitted with care.

0 comments on commit dda157e

Please sign in to comment.