Merge pull request #19 from hashicorp/f-cli

Revamped CLI
This commit is contained in:
Mitchell Hashimoto 2014-07-13 10:58:36 -07:00
commit af482381aa
34 changed files with 1919 additions and 253 deletions

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
*.dll
*.exe
example.tf
terraform.tfplan
terraform.tfstate
bin/
vendor/

View File

@ -110,12 +110,18 @@ func resource_aws_route_table_update(
break
}
// Append to the routes what we've done so far
resultRoutes = append(resultRoutes, map[string]string{
"cidr_block": op.Route.DestinationCidrBlock,
"gateway_id": op.Route.GatewayId,
"instance_id": op.Route.InstanceId,
})
// If we didn't delete the route, append it to the list of routes
// we have.
if op.Op != routeTableOpDelete {
resultMap := map[string]string{"cidr_block": op.Route.DestinationCidrBlock}
if op.Route.GatewayId != "" {
resultMap["gateway_id"] = op.Route.GatewayId
} else if op.Route.InstanceId != "" {
resultMap["instance_id"] = op.Route.InstanceId
}
resultRoutes = append(resultRoutes, resultMap)
}
}
// Update our state with the settings

View File

@ -1,57 +1,70 @@
package command
import (
"bytes"
"flag"
"fmt"
"os"
"sort"
"strings"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
// ApplyCommand is a Command implementation that applies a Terraform
// configuration and actually builds or changes infrastructure.
type ApplyCommand struct {
Meta
ShutdownCh <-chan struct{}
ContextOpts *terraform.ContextOpts
Ui cli.Ui
}
func (c *ApplyCommand) Run(args []string) int {
var init bool
var stateOutPath string
var statePath, stateOutPath string
args = c.Meta.process(args)
cmdFlags := flag.NewFlagSet("apply", flag.ContinueOnError)
cmdFlags.BoolVar(&init, "init", false, "init")
cmdFlags.StringVar(&stateOutPath, "out", "", "path")
cmdFlags.StringVar(&statePath, "state", DefaultStateFilename, "path")
cmdFlags.StringVar(&stateOutPath, "state-out", "", "path")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
}
var configPath string
args = cmdFlags.Args()
if len(args) != 2 {
c.Ui.Error("The apply command expects two arguments.\n")
if len(args) > 1 {
c.Ui.Error("The apply command expacts at most one argument.")
cmdFlags.Usage()
return 1
} else if len(args) == 1 {
configPath = args[0]
} else {
var err error
configPath, err = os.Getwd()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
}
}
statePath := args[0]
configPath := args[1]
// If we don't specify an output path, default to out normal state
// path.
if stateOutPath == "" {
stateOutPath = statePath
}
// The state path to use to generate a plan. If we're initializing
// a new infrastructure, then we don't use a state path.
planStatePath := statePath
if init {
planStatePath = ""
}
// Initialize Terraform right away
c.ContextOpts.Hooks = append(c.ContextOpts.Hooks, &UiHook{Ui: c.Ui})
ctx, err := ContextArg(configPath, planStatePath, c.ContextOpts)
// Build the context based on the arguments given
ctx, err := c.Context(configPath, planStatePath)
if err != nil {
c.Ui.Error(err.Error())
return 1
@ -60,6 +73,7 @@ func (c *ApplyCommand) Run(args []string) int {
return 1
}
// Start the apply in a goroutine so that we can be interrupted.
var state *terraform.State
var applyErr error
doneCh := make(chan struct{})
@ -68,6 +82,8 @@ func (c *ApplyCommand) Run(args []string) int {
state, applyErr = ctx.Apply()
}()
// Wait for the apply to finish or for us to be interrupted so
// we can handle it properly.
err = nil
select {
case <-c.ShutdownCh:
@ -106,25 +122,71 @@ func (c *ApplyCommand) Run(args []string) int {
return 1
}
c.Ui.Output(strings.TrimSpace(state.String()))
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
"[reset][bold][green]\n"+
"Apply succeeded! Infrastructure created and/or updated.\n"+
"The state of your infrastructure has been saved to the path\n"+
"below. This state is required to modify and destroy your\n"+
"infrastructure, so keep it safe. To inspect the complete state\n"+
"use the `terraform show` command.\n\n"+
"State path: %s",
stateOutPath)))
// If we have outputs, then output those at the end.
if len(state.Outputs) > 0 {
outputBuf := new(bytes.Buffer)
outputBuf.WriteString("[reset][bold][green]\nOutputs:\n\n")
// Output the outputs in alphabetical order
keyLen := 0
keys := make([]string, 0, len(state.Outputs))
for key, _ := range state.Outputs {
keys = append(keys, key)
if len(key) > keyLen {
keyLen = len(key)
}
}
sort.Strings(keys)
for _, k := range keys {
v := state.Outputs[k]
outputBuf.WriteString(fmt.Sprintf(
" %s%s = %s\n",
k,
strings.Repeat(" ", keyLen-len(k)),
v))
}
c.Ui.Output(c.Colorize().Color(
strings.TrimSpace(outputBuf.String())))
}
return 0
}
func (c *ApplyCommand) Help() string {
helpText := `
Usage: terraform apply [options] STATE PATH
Usage: terraform apply [options] [dir]
Builds or changes infrastructure according to the Terraform configuration
file.
Builds or changes infrastructure according to Terraform configuration
files .
Options:
-init If specified, it is okay to build brand new
infrastructure (with no state file specified).
-init If specified, new infrastructure can be built (no
previous state). This is just a safety switch
to prevent accidentally spinning up a new
infrastructure.
-out=file.tfstate Path to save the new state. If not specified, the
state path argument will be used.
-no-color If specified, output won't contain any color.
-state=path Path to read and save state (unless state-out
is specified). Defaults to "terraform.tfstate".
-state-out=path Path to write state to that is different than
"-state". This can be used to preserve the old
state.
`
return strings.TrimSpace(helpText)

View File

@ -2,7 +2,9 @@ package command
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"sync"
"testing"
@ -19,13 +21,15 @@ func TestApply(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
"-init",
statePath,
"-state", statePath,
testFixturePath("apply"),
}
if code := c.Run(args); code != 0 {
@ -55,13 +59,15 @@ func TestApply_configInvalid(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
"-init",
testTempFile(t),
"-state", testTempFile(t),
testFixturePath("apply-config-invalid"),
}
if code := c.Run(args); code != 1 {
@ -69,14 +75,69 @@ func TestApply_configInvalid(t *testing.T) {
}
}
func TestApply_defaultState(t *testing.T) {
td, err := ioutil.TempDir("", "tf")
if err != nil {
t.Fatalf("err: %s", err)
}
statePath := filepath.Join(td, DefaultStateFilename)
// Change to the temporary directory
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("err: %s", err)
}
if err := os.Chdir(filepath.Dir(statePath)); err != nil {
t.Fatalf("err: %s", err)
}
defer os.Chdir(cwd)
p := testProvider()
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
"-init",
testFixturePath("apply"),
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
if _, err := os.Stat(statePath); err != nil {
t.Fatalf("err: %s", err)
}
f, err := os.Open(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
defer f.Close()
state, err := terraform.ReadState(f)
if err != nil {
t.Fatalf("err: %s", err)
}
if state == nil {
t.Fatal("state should not be nil")
}
}
func TestApply_error(t *testing.T) {
statePath := testTempFile(t)
p := testProvider()
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
var lock sync.Mutex
@ -108,7 +169,7 @@ func TestApply_error(t *testing.T) {
args := []string{
"-init",
statePath,
"-state", statePath,
testFixturePath("apply-error"),
}
if code := c.Run(args); code != 1 {
@ -137,6 +198,54 @@ func TestApply_error(t *testing.T) {
}
}
func TestApply_noArgs(t *testing.T) {
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("err: %s", err)
}
if err := os.Chdir(testFixturePath("plan")); err != nil {
t.Fatalf("err: %s", err)
}
defer os.Chdir(cwd)
statePath := testTempFile(t)
p := testProvider()
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
"-init",
"-state", statePath,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
if _, err := os.Stat(statePath); err != nil {
t.Fatalf("err: %s", err)
}
f, err := os.Open(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
defer f.Close()
state, err := terraform.ReadState(f)
if err != nil {
t.Fatalf("err: %s", err)
}
if state == nil {
t.Fatal("state should not be nil")
}
}
func TestApply_plan(t *testing.T) {
planPath := testPlanFile(t, &terraform.Plan{
Config: new(config.Config),
@ -146,12 +255,14 @@ func TestApply_plan(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
statePath,
"-state", statePath,
planPath,
}
if code := c.Run(args); code != 0 {
@ -188,9 +299,12 @@ func TestApply_shutdown(t *testing.T) {
shutdownCh := make(chan struct{})
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
ShutdownCh: shutdownCh,
Ui: ui,
},
ShutdownCh: shutdownCh,
}
p.DiffFn = func(
@ -235,7 +349,7 @@ func TestApply_shutdown(t *testing.T) {
args := []string{
"-init",
statePath,
"-state", statePath,
testFixturePath("apply-shutdown"),
}
if code := c.Run(args); code != 0 {
@ -288,13 +402,15 @@ func TestApply_state(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
// Run the apply command pointing to our existing state
args := []string{
statePath,
"-state", statePath,
testFixturePath("apply"),
}
if code := c.Run(args); code != 0 {
@ -335,8 +451,10 @@ func TestApply_stateNoExist(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{

42
command/cli_ui.go Normal file
View File

@ -0,0 +1,42 @@
package command
import (
"fmt"
"github.com/mitchellh/cli"
"github.com/mitchellh/colorstring"
)
// ColoredUi is a Ui implementation that colors its output according
// to the given color schemes for the given type of output.
type ColorizeUi struct {
Colorize *colorstring.Colorize
OutputColor string
InfoColor string
ErrorColor string
Ui cli.Ui
}
func (u *ColorizeUi) Ask(query string) (string, error) {
return u.Ui.Ask(u.colorize(query, u.OutputColor))
}
func (u *ColorizeUi) Output(message string) {
u.Ui.Output(u.colorize(message, u.OutputColor))
}
func (u *ColorizeUi) Info(message string) {
u.Ui.Info(u.colorize(message, u.InfoColor))
}
func (u *ColorizeUi) Error(message string) {
u.Ui.Error(u.colorize(message, u.ErrorColor))
}
func (u *ColorizeUi) colorize(message string, color string) string {
if color == "" {
return message
}
return u.Colorize.Color(fmt.Sprintf("%s%s[reset]", color, message))
}

11
command/cli_ui_test.go Normal file
View File

@ -0,0 +1,11 @@
package command
import (
"testing"
"github.com/mitchellh/cli"
)
func TestColorizeUi_impl(t *testing.T) {
var _ cli.Ui = new(ColorizeUi)
}

View File

@ -2,74 +2,13 @@ package command
import (
"fmt"
"os"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
func ContextArg(
path string,
statePath string,
opts *terraform.ContextOpts) (*terraform.Context, error) {
// First try to just read the plan directly from the path given.
f, err := os.Open(path)
if err == nil {
plan, err := terraform.ReadPlan(f)
f.Close()
if err == nil {
return plan.Context(opts), nil
}
}
if statePath != "" {
if _, err := os.Stat(statePath); err != nil {
return nil, fmt.Errorf(
"There was an error reading the state file. The path\n"+
"and error are shown below. If you're trying to build a\n"+
"brand new infrastructure, explicitly pass the '-init'\n"+
"flag to Terraform to tell it it is okay to build new\n"+
"state.\n\n"+
"Path: %s\n"+
"Error: %s",
statePath,
err)
}
}
// Load up the state
var state *terraform.State
if statePath != "" {
f, err := os.Open(statePath)
if err == nil {
state, err = terraform.ReadState(f)
f.Close()
}
if err != nil {
return nil, fmt.Errorf("Error loading state: %s", err)
}
}
config, err := config.Load(path)
if err != nil {
return nil, fmt.Errorf("Error loading config: %s", err)
}
if err := config.Validate(); err != nil {
return nil, fmt.Errorf("Error validating config: %s", err)
}
opts.Config = config
opts.State = state
ctx := terraform.NewContext(opts)
if _, err := ctx.Plan(nil); err != nil {
return nil, fmt.Errorf("Error running plan: %s", err)
}
return ctx, nil
}
// DefaultStateFilename is the default filename used for the state file.
const DefaultStateFilename = "terraform.tfstate"
func validateContext(ctx *terraform.Context, ui cli.Ui) bool {
if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 {

View File

@ -10,10 +10,20 @@ import (
)
// This is the directory where our test fixtures are.
const fixtureDir = "./test-fixtures"
var fixtureDir = "./test-fixtures"
func init() {
// Expand the fixture dir on init because we change the working
// directory in some tests.
var err error
fixtureDir, err = filepath.Abs(fixtureDir)
if err != nil {
panic(err)
}
}
func testFixturePath(name string) string {
return filepath.Join(fixtureDir, name, "main.tf")
return filepath.Join(fixtureDir, name)
}
func testCtxConfig(p terraform.ResourceProvider) *terraform.ContextOpts {

101
command/format_plan.go Normal file
View File

@ -0,0 +1,101 @@
package command
import (
"bytes"
"fmt"
"sort"
"strings"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/colorstring"
)
// FormatPlan takes a plan and returns a
func FormatPlan(p *terraform.Plan, c *colorstring.Colorize) string {
if p.Diff == nil || p.Diff.Empty() {
return "This plan does nothing."
}
if c == nil {
c = &colorstring.Colorize{
Colors: colorstring.DefaultColors,
Reset: false,
}
}
buf := new(bytes.Buffer)
// We want to output the resources in sorted order to make things
// easier to scan through, so get all the resource names and sort them.
names := make([]string, 0, len(p.Diff.Resources))
for name, _ := range p.Diff.Resources {
names = append(names, name)
}
sort.Strings(names)
// Go through each sorted name and start building the output
for _, name := range names {
rdiff := p.Diff.Resources[name]
// Determine the color for the text (green for adding, yellow
// for change, red for delete), and symbol, and output the
// resource header.
color := "yellow"
symbol := "~"
if rdiff.RequiresNew() {
color = "green"
symbol = "+"
} else if rdiff.Destroy {
color = "red"
symbol = "-"
}
buf.WriteString(c.Color(fmt.Sprintf(
"[%s]%s %s\n",
color, symbol, name)))
// Get all the attributes that are changing, and sort them. Also
// determine the longest key so that we can align them all.
keyLen := 0
keys := make([]string, 0, len(rdiff.Attributes))
for key, _ := range rdiff.Attributes {
// Skip the ID since we do that specially
if key == "id" {
continue
}
keys = append(keys, key)
if len(key) > keyLen {
keyLen = len(key)
}
}
sort.Strings(keys)
// Go through and output each attribute
for _, attrK := range keys {
attrDiff := rdiff.Attributes[attrK]
v := attrDiff.New
if attrDiff.NewComputed {
v = "<computed>"
}
newResource := ""
if attrDiff.RequiresNew && rdiff.Destroy {
newResource = " (forces new resource)"
}
buf.WriteString(fmt.Sprintf(
" %s:%s %#v => %#v%s\n",
attrK,
strings.Repeat(" ", keyLen-len(attrK)),
attrDiff.Old,
v,
newResource))
}
// Write the reset color so we don't overload the user's terminal
buf.WriteString(c.Color("[reset]\n"))
}
return strings.TrimSpace(buf.String())
}

82
command/format_state.go Normal file
View File

@ -0,0 +1,82 @@
package command
import (
"bytes"
"fmt"
"sort"
"strings"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/colorstring"
)
// FormatState takes a state and returns a string
func FormatState(s *terraform.State, c *colorstring.Colorize) string {
if c == nil {
panic("colorize not given")
}
if len(s.Resources) == 0 {
return "The state file is empty. No resources are represented."
}
buf := new(bytes.Buffer)
buf.WriteString("[reset]")
// First get the names of all the resources so we can show them
// in alphabetical order.
names := make([]string, 0, len(s.Resources))
for name, _ := range s.Resources {
names = append(names, name)
}
sort.Strings(names)
// Go through each resource and begin building up the output.
for _, k := range names {
rs := s.Resources[k]
id := rs.ID
if id == "" {
id = "<not created>"
}
buf.WriteString(fmt.Sprintf("%s:\n", k))
buf.WriteString(fmt.Sprintf(" id = %s\n", id))
// Sort the attributes
attrKeys := make([]string, 0, len(rs.Attributes))
for ak, _ := range rs.Attributes {
// Skip the id attribute since we just show the id directly
if ak == "id" {
continue
}
attrKeys = append(attrKeys, ak)
}
sort.Strings(attrKeys)
// Output each attribute
for _, ak := range attrKeys {
av := rs.Attributes[ak]
buf.WriteString(fmt.Sprintf(" %s = %s\n", ak, av))
}
}
if len(s.Outputs) > 0 {
buf.WriteString("\nOutputs:\n\n")
// Sort the outputs
ks := make([]string, 0, len(s.Outputs))
for k, _ := range s.Outputs {
ks = append(ks, k)
}
sort.Strings(ks)
// Output each output k/v pair
for _, k := range ks {
v := s.Outputs[k]
buf.WriteString(fmt.Sprintf("%s = %s\n", k, v))
}
}
return c.Color(strings.TrimSpace(buf.String()))
}

View File

@ -1,58 +1,66 @@
package command
import (
"bytes"
"flag"
"fmt"
"os"
"strings"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/digraph"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
// GraphCommand is a Command implementation that takes a Terraform
// configuration and outputs the dependency tree in graphical form.
type GraphCommand struct {
ContextOpts *terraform.ContextOpts
Ui cli.Ui
Meta
}
func (c *GraphCommand) Run(args []string) int {
args = c.Meta.process(args)
cmdFlags := flag.NewFlagSet("graph", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
}
var path string
args = cmdFlags.Args()
if len(args) != 1 {
if len(args) > 1 {
c.Ui.Error("The graph command expects one argument.\n")
cmdFlags.Usage()
return 1
} else if len(args) == 1 {
path = args[0]
} else {
var err error
path, err = os.Getwd()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
}
}
conf, err := config.Load(args[0])
ctx, err := c.Context(path, "")
if err != nil {
c.Ui.Error(fmt.Sprintf("Error loading config: %s", err))
c.Ui.Error(fmt.Sprintf("Error loading Terraform: %s", err))
return 1
}
g, err := terraform.Graph(&terraform.GraphOpts{
Config: conf,
Providers: c.ContextOpts.Providers,
})
g, err := ctx.Graph()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error creating graph: %s", err))
return 1
}
buf := new(bytes.Buffer)
nodes := make([]digraph.Node, len(g.Nouns))
for i, n := range g.Nouns {
nodes[i] = n
}
digraph.GenerateDot(nodes, os.Stdout)
digraph.GenerateDot(nodes, buf)
c.Ui.Output(buf.String())
return 0
}
@ -66,10 +74,14 @@ Usage: terraform graph [options] PATH
shown. If the path is a plan file, then the dependency graph of the
plan itself is shown.
The graph is outputted in DOT format. The typical program that can
read this format is GraphViz, but many web services are also available
to read this format.
`
return strings.TrimSpace(helpText)
}
func (c *GraphCommand) Synopsis() string {
return "Output visual graph of Terraform resources"
return "Create a visual graph of Terraform resources"
}

106
command/graph_test.go Normal file
View File

@ -0,0 +1,106 @@
package command
import (
"os"
"strings"
"testing"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
func TestGraph(t *testing.T) {
ui := new(cli.MockUi)
c := &GraphCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{
testFixturePath("graph"),
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
output := ui.OutputWriter.String()
if !strings.Contains(output, "digraph {") {
t.Fatalf("doesn't look like digraph: %s", output)
}
}
func TestGraph_multipleArgs(t *testing.T) {
ui := new(cli.MockUi)
c := &GraphCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{
"bad",
"bad",
}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: \n%s", ui.OutputWriter.String())
}
}
func TestGraph_noArgs(t *testing.T) {
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("err: %s", err)
}
if err := os.Chdir(testFixturePath("graph")); err != nil {
t.Fatalf("err: %s", err)
}
defer os.Chdir(cwd)
ui := new(cli.MockUi)
c := &GraphCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
output := ui.OutputWriter.String()
if !strings.Contains(output, "digraph {") {
t.Fatalf("doesn't look like digraph: %s", output)
}
}
func TestGraph_plan(t *testing.T) {
planPath := testPlanFile(t, &terraform.Plan{
Config: new(config.Config),
})
ui := new(cli.MockUi)
c := &GraphCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{
planPath,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
output := ui.OutputWriter.String()
if !strings.Contains(output, "digraph {") {
t.Fatalf("doesn't look like digraph: %s", output)
}
}

View File

@ -1,16 +1,21 @@
package command
import (
"bytes"
"fmt"
"sort"
"strings"
"sync"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
"github.com/mitchellh/colorstring"
)
type UiHook struct {
terraform.NilHook
Colorize *colorstring.Colorize
Ui cli.Ui
once sync.Once
@ -23,15 +28,65 @@ func (h *UiHook) PreApply(
d *terraform.ResourceDiff) (terraform.HookAction, error) {
h.once.Do(h.init)
h.ui.Output(fmt.Sprintf("%s: Applying...", id))
operation := "Modifying..."
if d.Destroy {
operation = "Destroying..."
} else if s.ID == "" {
operation = "Creating..."
}
attrBuf := new(bytes.Buffer)
// Get all the attributes that are changing, and sort them. Also
// determine the longest key so that we can align them all.
keyLen := 0
keys := make([]string, 0, len(d.Attributes))
for key, _ := range d.Attributes {
// Skip the ID since we do that specially
if key == "id" {
continue
}
keys = append(keys, key)
if len(key) > keyLen {
keyLen = len(key)
}
}
sort.Strings(keys)
// Go through and output each attribute
for _, attrK := range keys {
attrDiff := d.Attributes[attrK]
v := attrDiff.New
if attrDiff.NewComputed {
v = "<computed>"
}
attrBuf.WriteString(fmt.Sprintf(
" %s:%s %#v => %#v\n",
attrK,
strings.Repeat(" ", keyLen-len(attrK)),
attrDiff.Old,
v))
}
attrString := strings.TrimSpace(attrBuf.String())
if attrString != "" {
attrString = "\n " + attrString
}
h.ui.Output(h.Colorize.Color(fmt.Sprintf(
"[reset][bold]%s: %s[reset_bold]%s",
id,
operation,
attrString)))
return terraform.HookActionContinue, nil
}
func (h *UiHook) PreDiff(
id string, s *terraform.ResourceState) (terraform.HookAction, error) {
h.once.Do(h.init)
h.ui.Output(fmt.Sprintf("%s: Calculating diff", id))
return terraform.HookActionContinue, nil
}
@ -39,11 +94,17 @@ func (h *UiHook) PreRefresh(
id string, s *terraform.ResourceState) (terraform.HookAction, error) {
h.once.Do(h.init)
h.ui.Output(fmt.Sprintf("%s: Refreshing state (ID: %s)", id, s.ID))
h.ui.Output(h.Colorize.Color(fmt.Sprintf(
"[reset][bold]%s: Refreshing (ID: %s)",
id, s.ID)))
return terraform.HookActionContinue, nil
}
func (h *UiHook) init() {
if h.Colorize == nil {
panic("colorize not given")
}
// Wrap the ui so that it is safe for concurrency regardless of the
// underlying reader/writer that is in place.
h.ui = &cli.ConcurrentUi{Ui: h.Ui}

142
command/meta.go Normal file
View File

@ -0,0 +1,142 @@
package command
import (
"fmt"
"os"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
"github.com/mitchellh/colorstring"
)
// Meta are the meta-options that are available on all or most commands.
type Meta struct {
Color bool
ContextOpts *terraform.ContextOpts
Ui cli.Ui
oldUi cli.Ui
}
// Colorize returns the colorization structure for a command.
func (m *Meta) Colorize() *colorstring.Colorize {
return &colorstring.Colorize{
Colors: colorstring.DefaultColors,
Disable: !m.Color,
Reset: true,
}
}
// Context returns a Terraform Context taking into account the context
// options used to initialize this meta configuration.
func (m *Meta) Context(path, statePath string) (*terraform.Context, error) {
opts := m.contextOpts()
// First try to just read the plan directly from the path given.
f, err := os.Open(path)
if err == nil {
plan, err := terraform.ReadPlan(f)
f.Close()
if err == nil {
return plan.Context(opts), nil
}
}
if statePath != "" {
if _, err := os.Stat(statePath); err != nil {
return nil, fmt.Errorf(
"There was an error reading the state file. The path\n"+
"and error are shown below. If you're trying to build a\n"+
"brand new infrastructure, explicitly pass the '-init'\n"+
"flag to Terraform to tell it it is okay to build new\n"+
"state.\n\n"+
"Path: %s\n"+
"Error: %s",
statePath,
err)
}
}
// Load up the state
var state *terraform.State
if statePath != "" {
f, err := os.Open(statePath)
if err == nil {
state, err = terraform.ReadState(f)
f.Close()
}
if err != nil {
return nil, fmt.Errorf("Error loading state: %s", err)
}
}
config, err := config.LoadDir(path)
if err != nil {
return nil, fmt.Errorf("Error loading config: %s", err)
}
if err := config.Validate(); err != nil {
return nil, fmt.Errorf("Error validating config: %s", err)
}
opts.Config = config
opts.State = state
ctx := terraform.NewContext(opts)
if _, err := ctx.Plan(nil); err != nil {
return nil, fmt.Errorf("Error running plan: %s", err)
}
return ctx, nil
}
// contextOpts returns the options to use to initialize a Terraform
// context with the settings from this Meta.
func (m *Meta) contextOpts() *terraform.ContextOpts {
var opts terraform.ContextOpts = *m.ContextOpts
opts.Hooks = make([]terraform.Hook, len(m.ContextOpts.Hooks)+1)
opts.Hooks[0] = m.uiHook()
copy(opts.Hooks[1:], m.ContextOpts.Hooks)
return &opts
}
// process will process the meta-parameters out of the arguments. This
// will potentially modify the args in-place. It will return the resulting
// slice.
func (m *Meta) process(args []string) []string {
// We do this so that we retain the ability to technically call
// process multiple times, even if we have no plans to do so
if m.oldUi != nil {
m.Ui = m.oldUi
}
// Set colorization
m.Color = true
for i, v := range args {
if v == "-no-color" {
m.Color = false
args = append(args[:i], args[i+1:]...)
break
}
}
// Set the UI
m.oldUi = m.Ui
m.Ui = &ColorizeUi{
Colorize: m.Colorize(),
ErrorColor: "[red]",
Ui: m.oldUi,
}
return args
}
// uiHook returns the UiHook to use with the context.
func (m *Meta) uiHook() *UiHook {
return &UiHook{
Colorize: m.Colorize(),
Ui: m.Ui,
}
}

35
command/meta_test.go Normal file
View File

@ -0,0 +1,35 @@
package command
import (
"reflect"
"testing"
)
func TestMetaColorize(t *testing.T) {
var m *Meta
var args, args2 []string
// Test basic, no change
m = new(Meta)
args = []string{"foo", "bar"}
args2 = []string{"foo", "bar"}
args = m.process(args)
if !reflect.DeepEqual(args, args2) {
t.Fatalf("bad: %#v", args)
}
if m.Colorize().Disable {
t.Fatal("should not be disabled")
}
// Test disable #1
m = new(Meta)
args = []string{"foo", "-no-color", "bar"}
args2 = []string{"foo", "bar"}
args = m.process(args)
if !reflect.DeepEqual(args, args2) {
t.Fatalf("bad: %#v", args)
}
if !m.Colorize().Disable {
t.Fatal("should be disabled")
}
}

92
command/output.go Normal file
View File

@ -0,0 +1,92 @@
package command
import (
"flag"
"fmt"
"os"
"strings"
"github.com/hashicorp/terraform/terraform"
)
// OutputCommand is a Command implementation that reads an output
// from a Terraform state and prints it.
type OutputCommand struct {
Meta
}
func (c *OutputCommand) Run(args []string) int {
var statePath string
args = c.Meta.process(args)
cmdFlags := flag.NewFlagSet("output", flag.ContinueOnError)
cmdFlags.StringVar(&statePath, "state", DefaultStateFilename, "path")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
}
args = cmdFlags.Args()
if len(args) != 1 || args[0] == "" {
c.Ui.Error(
"The output command expects exactly one argument with the name\n" +
"of an output variable.\n")
cmdFlags.Usage()
return 1
}
name := args[0]
f, err := os.Open(statePath)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error loading file: %s", err))
return 1
}
state, err := terraform.ReadState(f)
f.Close()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error reading state: %s", err))
return 1
}
if len(state.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
}
v, ok := state.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
}
c.Ui.Output(v)
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.
Options:
-state=path Path to the state file to read. Defaults to
"terraform.tfstate".
`
return strings.TrimSpace(helpText)
}
func (c *OutputCommand) Synopsis() string {
return "Read an output from a state file"
}

208
command/output_test.go Normal file
View File

@ -0,0 +1,208 @@
package command
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
func TestOutput(t *testing.T) {
originalState := &terraform.State{
Outputs: map[string]string{
"foo": "bar",
},
}
statePath := testStateFile(t, originalState)
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{
"-state", statePath,
"foo",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
actual := strings.TrimSpace(ui.OutputWriter.String())
if actual != "bar" {
t.Fatalf("bad: %#v", actual)
}
}
func TestOutput_badVar(t *testing.T) {
originalState := &terraform.State{
Outputs: map[string]string{
"foo": "bar",
},
}
statePath := testStateFile(t, originalState)
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{
"-state", statePath,
"bar",
}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
}
func TestOutput_blank(t *testing.T) {
originalState := &terraform.State{
Outputs: map[string]string{
"foo": "bar",
},
}
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 != 1 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
}
func TestOutput_manyArgs(t *testing.T) {
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{
"bad",
"bad",
}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: \n%s", ui.OutputWriter.String())
}
}
func TestOutput_noArgs(t *testing.T) {
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: \n%s", ui.OutputWriter.String())
}
}
func TestOutput_noVars(t *testing.T) {
originalState := &terraform.State{
Outputs: map[string]string{},
}
statePath := testStateFile(t, originalState)
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{
"-state", statePath,
"bar",
}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
}
func TestOutput_stateDefault(t *testing.T) {
originalState := &terraform.State{
Outputs: map[string]string{
"foo": "bar",
},
}
// Write the state file in a temporary directory with the
// default filename.
td, err := ioutil.TempDir("", "tf")
if err != nil {
t.Fatalf("err: %s", err)
}
statePath := filepath.Join(td, DefaultStateFilename)
f, err := os.Create(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
err = terraform.WriteState(originalState, f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
// Change to that directory
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("err: %s", err)
}
if err := os.Chdir(filepath.Dir(statePath)); err != nil {
t.Fatalf("err: %s", err)
}
defer os.Chdir(cwd)
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{
"foo",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
actual := strings.TrimSpace(ui.OutputWriter.String())
if actual != "bar" {
t.Fatalf("bad: %#v", actual)
}
}

View File

@ -7,77 +7,74 @@ import (
"os"
"strings"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
// PlanCommand is a Command implementation that compares a Terraform
// configuration to an actual infrastructure and shows the differences.
type PlanCommand struct {
ContextOpts *terraform.ContextOpts
Ui cli.Ui
Meta
}
func (c *PlanCommand) Run(args []string) int {
var destroy, refresh bool
var outPath, statePath string
args = c.Meta.process(args)
cmdFlags := flag.NewFlagSet("plan", flag.ContinueOnError)
cmdFlags.BoolVar(&destroy, "destroy", false, "destroy")
cmdFlags.BoolVar(&refresh, "refresh", true, "refresh")
cmdFlags.StringVar(&outPath, "out", "", "path")
cmdFlags.StringVar(&statePath, "state", "", "path")
cmdFlags.StringVar(&statePath, "state", DefaultStateFilename, "path")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
}
var path string
args = cmdFlags.Args()
if len(args) != 1 {
if len(args) > 1 {
c.Ui.Error(
"The plan command expects only one argument with the path\n" +
"The plan command expects at most one argument with the path\n" +
"to a Terraform configuration.\n")
cmdFlags.Usage()
return 1
} else if len(args) == 1 {
path = args[0]
} else {
var err error
path, err = os.Getwd()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
}
}
// Load up the state
var state *terraform.State
// If the default state path doesn't exist, ignore it.
if statePath != "" {
f, err := os.Open(statePath)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error loading state: %s", err))
return 1
if _, err := os.Stat(statePath); err != nil {
if os.IsNotExist(err) && statePath == DefaultStateFilename {
statePath = ""
}
state, err = terraform.ReadState(f)
f.Close()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error loading state: %s", err))
return 1
}
}
b, err := config.Load(args[0])
ctx, err := c.Context(path, statePath)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error loading blueprint: %s", err))
c.Ui.Error(err.Error())
return 1
}
c.ContextOpts.Config = b
c.ContextOpts.Hooks = append(c.ContextOpts.Hooks, &UiHook{Ui: c.Ui})
c.ContextOpts.State = state
ctx := terraform.NewContext(c.ContextOpts)
if !validateContext(ctx, c.Ui) {
return 1
}
if refresh {
c.Ui.Output("Refreshing Terraform state prior to plan...\n")
if _, err := ctx.Refresh(); err != nil {
c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err))
return 1
}
c.Ui.Output("")
}
plan, err := ctx.Plan(&terraform.PlanOpts{Destroy: destroy})
@ -87,12 +84,14 @@ func (c *PlanCommand) Run(args []string) int {
}
if plan.Diff.Empty() {
c.Ui.Output("No changes. Infrastructure is up-to-date.")
c.Ui.Output(
"No changes. Infrastructure is up-to-date. This means that Terraform\n" +
"could not detect any differences between your configuration and\n" +
"the real physical resources that exist. As a result, Terraform\n" +
"doesn't need to do anything.")
return 0
}
c.Ui.Output(strings.TrimSpace(plan.String()))
if outPath != "" {
log.Printf("[INFO] Writing plan output to: %s", outPath)
f, err := os.Create(outPath)
@ -106,33 +105,75 @@ func (c *PlanCommand) Run(args []string) int {
}
}
if outPath == "" {
c.Ui.Output(strings.TrimSpace(planHeaderNoOutput) + "\n")
} else {
c.Ui.Output(fmt.Sprintf(
strings.TrimSpace(planHeaderYesOutput)+"\n",
outPath))
}
c.Ui.Output(FormatPlan(plan, c.Colorize()))
return 0
}
func (c *PlanCommand) Help() string {
helpText := `
Usage: terraform plan [options] [terraform.tf]
Usage: terraform plan [options] [dir]
Shows the differences between the Terraform configuration and
the actual state of an infrastructure.
Generates an execution plan for Terraform.
This execution plan can be reviewed prior to running apply to get a
sense for what Terraform will do. Optionally, the plan can be saved to
a Terraform plan file, and apply can take this plan file to execute
this plan exactly.
Options:
-destroy If set, a plan will be generated to destroy all resources
managed by the given configuration and state.
-no-color If specified, output won't contain any color.
-out=path Write a plan file to the given path. This can be used as
input to the "apply" command.
-refresh=true Update state prior to checking for differences.
-state=statefile Path to a Terraform state file to use to look
up Terraform-managed resources.
up Terraform-managed resources. By default it will
use the state "terraform.tfstate" if it exists.
`
return strings.TrimSpace(helpText)
}
func (c *PlanCommand) Synopsis() string {
return "Show changes between Terraform config and infrastructure"
return "Generate and show an execution plan"
}
const planHeaderNoOutput = `
The Terraform execution plan has been generated and is shown below.
Resources are shown in alphabetical order for quick scanning. Green resources
will be created (or destroyed and then created if an existing resource
exists), yellow resources are being changed in-place, and red resources
will be destroyed.
Note: You didn't specify an "-out" parameter to save this plan, so when
"apply" is called, Terraform can't guarantee this is what will execute.
`
const planHeaderYesOutput = `
The Terraform execution plan has been generated and is shown below.
Resources are shown in alphabetical order for quick scanning. Green resources
will be created (or destroyed and then created if an existing resource
exists), yellow resources are being changed in-place, and red resources
will be destroyed.
Your plan was also saved to the path below. Call the "apply" subcommand
with this plan file and Terraform will exactly execute this execution
plan.
Path: %s
`

View File

@ -3,6 +3,7 @@ package command
import (
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
@ -10,6 +11,31 @@ import (
"github.com/mitchellh/cli"
)
func TestPlan(t *testing.T) {
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("err: %s", err)
}
if err := os.Chdir(testFixturePath("plan")); err != nil {
t.Fatalf("err: %s", err)
}
defer os.Chdir(cwd)
p := testProvider()
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
}
func TestPlan_destroy(t *testing.T) {
originalState := &terraform.State{
Resources: map[string]*terraform.ResourceState{
@ -26,8 +52,10 @@ func TestPlan_destroy(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
@ -55,8 +83,10 @@ func TestPlan_noState(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
@ -91,8 +121,10 @@ func TestPlan_outPath(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
p.DiffReturn = &terraform.ResourceDiff{
@ -122,8 +154,10 @@ func TestPlan_refresh(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
@ -166,8 +200,10 @@ func TestPlan_state(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
@ -184,3 +220,64 @@ func TestPlan_state(t *testing.T) {
t.Fatalf("bad: %#v", p.DiffState)
}
}
func TestPlan_stateDefault(t *testing.T) {
originalState := &terraform.State{
Resources: map[string]*terraform.ResourceState{
"test_instance.foo": &terraform.ResourceState{
ID: "bar",
Type: "test_instance",
},
},
}
// Write the state file in a temporary directory with the
// default filename.
td, err := ioutil.TempDir("", "tf")
if err != nil {
t.Fatalf("err: %s", err)
}
statePath := filepath.Join(td, DefaultStateFilename)
f, err := os.Create(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
err = terraform.WriteState(originalState, f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
// Change to that directory
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("err: %s", err)
}
if err := os.Chdir(filepath.Dir(statePath)); err != nil {
t.Fatalf("err: %s", err)
}
defer os.Chdir(cwd)
p := testProvider()
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
testFixturePath("plan"),
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
// Verify that the provider was called with the existing state
expectedState := originalState.Resources["test_instance.foo"]
if !reflect.DeepEqual(p.DiffState, expectedState) {
t.Fatalf("bad: %#v", p.DiffState)
}
}

View File

@ -7,81 +7,94 @@ import (
"os"
"strings"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
// RefreshCommand is a cli.Command implementation that refreshes the state
// file.
type RefreshCommand struct {
ContextOpts *terraform.ContextOpts
Ui cli.Ui
Meta
}
func (c *RefreshCommand) Run(args []string) int {
var outPath string
statePath := "terraform.tfstate"
configPath := "."
var statePath, stateOutPath string
args = c.Meta.process(args)
cmdFlags := flag.NewFlagSet("refresh", flag.ContinueOnError)
cmdFlags.StringVar(&outPath, "out", "", "output path")
cmdFlags.StringVar(&statePath, "state", DefaultStateFilename, "path")
cmdFlags.StringVar(&stateOutPath, "state-out", "", "path")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
}
var configPath string
args = cmdFlags.Args()
if len(args) != 2 {
// TODO(mitchellh): this is temporary until we can assume current
// dir for Terraform config.
c.Ui.Error("TEMPORARY: The refresh command requires two args.")
if len(args) > 1 {
c.Ui.Error("The apply command expacts at most one argument.")
cmdFlags.Usage()
return 1
}
statePath = args[0]
configPath = args[1]
if outPath == "" {
outPath = statePath
}
// Load up the state
f, err := os.Open(statePath)
} else if len(args) == 1 {
configPath = args[0]
} else {
var err error
configPath, err = os.Getwd()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error loading state: %s", err))
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
}
}
// If we don't specify an output path, default to out normal state
// path.
if stateOutPath == "" {
stateOutPath = statePath
}
// Verify that the state path exists. The "ContextArg" function below
// will actually do this, but we want to provide a richer error message
// if possible.
if _, err := os.Stat(statePath); err != nil {
if os.IsNotExist(err) {
c.Ui.Error(fmt.Sprintf(
"The Terraform state file for your infrastructure does not\n"+
"exist. The 'refresh' command only works and only makes sense\n"+
"when there is existing state that Terraform is managing. Please\n"+
"double-check the value given below and try again. If you\n"+
"haven't created infrastructure with Terraform yet, use the\n"+
"'terraform apply' command.\n\n"+
"Path: %s",
statePath))
return 1
}
state, err := terraform.ReadState(f)
f.Close()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error loading state: %s", err))
c.Ui.Error(fmt.Sprintf(
"There was an error reading the Terraform state that is needed\n"+
"for refreshing. The path and error are shown below.\n\n"+
"Path: %s\n\nError: %s",
statePath,
err))
return 1
}
b, err := config.Load(configPath)
// Build the context based on the arguments given
ctx, err := c.Context(configPath, statePath)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error loading blueprint: %s", err))
c.Ui.Error(err.Error())
return 1
}
c.ContextOpts.Config = b
c.ContextOpts.State = state
c.ContextOpts.Hooks = append(c.ContextOpts.Hooks, &UiHook{Ui: c.Ui})
ctx := terraform.NewContext(c.ContextOpts)
if !validateContext(ctx, c.Ui) {
return 1
}
state, err = ctx.Refresh()
state, err := ctx.Refresh()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err))
return 1
}
log.Printf("[INFO] Writing state output to: %s", outPath)
f, err = os.Create(outPath)
log.Printf("[INFO] Writing state output to: %s", stateOutPath)
f, err := os.Create(stateOutPath)
if err == nil {
defer f.Close()
err = terraform.WriteState(state, f)
@ -96,21 +109,29 @@ func (c *RefreshCommand) Run(args []string) int {
func (c *RefreshCommand) Help() string {
helpText := `
Usage: terraform refresh [options] [terraform.tfstate] [terraform.tf]
Usage: terraform refresh [options] [dir]
Refresh and update the state of your infrastructure. This is read-only
operation that will not modify infrastructure. The read-only property
is dependent on resource providers being implemented correctly.
Update the state file of your infrastructure with metadata that matches
the physical resources they are tracking.
This will not modify your infrastructure, but it can modify your
state file to update metadata. This metadata might cause new changes
to occur when you generate a plan or call apply next.
Options:
-out=path Path to write updated state file. If this is not specified,
the existing state file will be overridden.
-no-color If specified, output won't contain any color.
-state=path Path to read and save state (unless state-out
is specified). Defaults to "terraform.tfstate".
-state-out=path Path to write updated state file. By default, the
"-state" path will be used.
`
return strings.TrimSpace(helpText)
}
func (c *RefreshCommand) Synopsis() string {
return "Refresh the state of your infrastructure"
return "Update local state file against real resources"
}

View File

@ -3,6 +3,7 @@ package command
import (
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
@ -24,15 +25,17 @@ func TestRefresh(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
p.RefreshFn = nil
p.RefreshReturn = &terraform.ResourceState{ID: "yes"}
args := []string{
statePath,
"-state", statePath,
testFixturePath("refresh"),
}
if code := c.Run(args); code != 0 {
@ -61,6 +64,165 @@ func TestRefresh(t *testing.T) {
}
}
func TestRefresh_badState(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
"-state", "i-should-not-exist-ever",
testFixturePath("refresh"),
}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
}
func TestRefresh_cwd(t *testing.T) {
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("err: %s", err)
}
if err := os.Chdir(testFixturePath("refresh")); err != nil {
t.Fatalf("err: %s", err)
}
defer os.Chdir(cwd)
state := &terraform.State{
Resources: map[string]*terraform.ResourceState{
"test_instance.foo": &terraform.ResourceState{
ID: "bar",
Type: "test_instance",
},
},
}
statePath := testStateFile(t, state)
p := testProvider()
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
p.RefreshFn = nil
p.RefreshReturn = &terraform.ResourceState{ID: "yes"}
args := []string{
"-state", statePath,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
if !p.RefreshCalled {
t.Fatal("refresh should be called")
}
f, err := os.Open(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
newState, err := terraform.ReadState(f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
actual := newState.Resources["test_instance.foo"]
expected := p.RefreshReturn
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}
func TestRefresh_defaultState(t *testing.T) {
originalState := &terraform.State{
Resources: map[string]*terraform.ResourceState{
"test_instance.foo": &terraform.ResourceState{
ID: "bar",
Type: "test_instance",
},
},
}
// Write the state file in a temporary directory with the
// default filename.
td, err := ioutil.TempDir("", "tf")
if err != nil {
t.Fatalf("err: %s", err)
}
statePath := filepath.Join(td, DefaultStateFilename)
f, err := os.Create(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
err = terraform.WriteState(originalState, f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
// Change to that directory
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("err: %s", err)
}
if err := os.Chdir(filepath.Dir(statePath)); err != nil {
t.Fatalf("err: %s", err)
}
defer os.Chdir(cwd)
p := testProvider()
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
p.RefreshFn = nil
p.RefreshReturn = &terraform.ResourceState{ID: "yes"}
args := []string{
testFixturePath("refresh"),
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
if !p.RefreshCalled {
t.Fatal("refresh should be called")
}
f, err = os.Open(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
newState, err := terraform.ReadState(f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
actual := newState.Resources["test_instance.foo"]
expected := p.RefreshReturn
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}
func TestRefresh_outPath(t *testing.T) {
state := &terraform.State{
Resources: map[string]*terraform.ResourceState{
@ -84,16 +246,18 @@ func TestRefresh_outPath(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
p.RefreshFn = nil
p.RefreshReturn = &terraform.ResourceState{ID: "yes"}
args := []string{
"-out", outPath,
statePath,
"-state", statePath,
"-state-out", outPath,
testFixturePath("refresh"),
}
if code := c.Run(args); code != 0 {

100
command/show.go Normal file
View File

@ -0,0 +1,100 @@
package command
import (
"flag"
"fmt"
"os"
"strings"
"github.com/hashicorp/terraform/terraform"
)
// ShowCommand is a Command implementation that reads and outputs the
// contents of a Terraform plan or state file.
type ShowCommand struct {
Meta
}
func (c *ShowCommand) Run(args []string) int {
args = c.Meta.process(args)
cmdFlags := flag.NewFlagSet("show", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
}
args = cmdFlags.Args()
if len(args) != 1 {
c.Ui.Error(
"The show command expects exactly one argument with the path\n" +
"to a Terraform state or plan file.\n")
cmdFlags.Usage()
return 1
}
path := args[0]
var plan *terraform.Plan
var state *terraform.State
f, err := os.Open(path)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error loading file: %s", err))
return 1
}
var planErr, stateErr error
plan, err = terraform.ReadPlan(f)
if err != nil {
if _, err := f.Seek(0, 0); err != nil {
c.Ui.Error(fmt.Sprintf("Error reading file: %s", err))
return 1
}
plan = nil
planErr = err
}
if plan == nil {
state, err = terraform.ReadState(f)
if err != nil {
stateErr = err
}
}
if plan == nil && state == nil {
c.Ui.Error(fmt.Sprintf(
"Terraform couldn't read the given file as a state or plan file.\n"+
"The errors while attempting to read the file as each format are\n"+
"shown below.\n\n"+
"State read error: %s\n\nPlan read error: %s",
stateErr,
planErr))
return 1
}
if plan != nil {
c.Ui.Output(FormatPlan(plan, c.Colorize()))
return 0
}
c.Ui.Output(FormatState(state, c.Colorize()))
return 0
}
func (c *ShowCommand) Help() string {
helpText := `
Usage: terraform show [options] path
Reads and outputs a Terraform state or plan file in a human-readable
form.
Options:
-no-color If specified, output won't contain any color.
`
return strings.TrimSpace(helpText)
}
func (c *ShowCommand) Synopsis() string {
return "Inspect Terraform state or plan"
}

91
command/show_test.go Normal file
View File

@ -0,0 +1,91 @@
package command
import (
"testing"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
func TestShow(t *testing.T) {
ui := new(cli.MockUi)
c := &ShowCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{
"bad",
"bad",
}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: \n%s", ui.OutputWriter.String())
}
}
func TestShow_noArgs(t *testing.T) {
ui := new(cli.MockUi)
c := &ShowCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: \n%s", ui.OutputWriter.String())
}
}
func TestShow_plan(t *testing.T) {
planPath := testPlanFile(t, &terraform.Plan{
Config: new(config.Config),
})
ui := new(cli.MockUi)
c := &ShowCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{
planPath,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
}
func TestShow_state(t *testing.T) {
originalState := &terraform.State{
Resources: map[string]*terraform.ResourceState{
"test_instance.foo": &terraform.ResourceState{
ID: "bar",
Type: "test_instance",
},
},
}
statePath := testStateFile(t, originalState)
ui := new(cli.MockUi)
c := &ShowCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{
statePath,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
}

View File

@ -0,0 +1,3 @@
resource "test_instance" "foo" {
ami = "bar"
}

View File

@ -3,24 +3,26 @@ package command
import (
"bytes"
"fmt"
"github.com/mitchellh/cli"
)
// VersionCommand is a Command implementation prints the version.
type VersionCommand struct {
Meta
Revision string
Version string
VersionPrerelease string
Ui cli.Ui
}
func (c *VersionCommand) Help() string {
return ""
}
func (c *VersionCommand) Run(_ []string) int {
func (c *VersionCommand) Run(args []string) int {
var versionString bytes.Buffer
args = c.Meta.process(args)
fmt.Fprintf(&versionString, "Terraform v%s", c.Version)
if c.VersionPrerelease != "" {
fmt.Fprintf(&versionString, ".%s", c.VersionPrerelease)

View File

@ -26,42 +26,55 @@ func init() {
Ui: &cli.BasicUi{Writer: os.Stdout},
}
meta := command.Meta{
ContextOpts: &ContextOpts,
Ui: Ui,
}
Commands = map[string]cli.CommandFactory{
"apply": func() (cli.Command, error) {
return &command.ApplyCommand{
Meta: meta,
ShutdownCh: makeShutdownCh(),
ContextOpts: &ContextOpts,
Ui: Ui,
}, nil
},
"graph": func() (cli.Command, error) {
return &command.GraphCommand{
ContextOpts: &ContextOpts,
Ui: Ui,
Meta: meta,
}, nil
},
"output": func() (cli.Command, error) {
return &command.OutputCommand{
Meta: meta,
}, nil
},
"plan": func() (cli.Command, error) {
return &command.PlanCommand{
ContextOpts: &ContextOpts,
Ui: Ui,
Meta: meta,
}, nil
},
"refresh": func() (cli.Command, error) {
return &command.RefreshCommand{
ContextOpts: &ContextOpts,
Ui: Ui,
Meta: meta,
}, nil
},
"show": func() (cli.Command, error) {
return &command.ShowCommand{
Meta: meta,
}, nil
},
"version": func() (cli.Command, error) {
return &command.VersionCommand{
Meta: meta,
Revision: GitCommit,
Version: Version,
VersionPrerelease: VersionPrerelease,
Ui: Ui,
}, nil
},
}

View File

@ -1,6 +1,7 @@
package config
import (
"fmt"
"path/filepath"
)
@ -35,6 +36,12 @@ func LoadDir(path string) (*Config, error) {
return nil, err
}
if len(matches) == 0 {
return nil, fmt.Errorf(
"No Terraform configuration files found in directory: %s",
path)
}
var result *Config
for _, f := range matches {
c, err := Load(f)

View File

@ -128,6 +128,13 @@ func TestLoadDir_basic(t *testing.T) {
}
}
func TestLoadDir_noConfigs(t *testing.T) {
_, err := LoadDir(filepath.Join(fixtureDir, "dir-empty"))
if err == nil {
t.Fatal("should error")
}
}
func outputsStr(os map[string]*Output) string {
ns := make([]string, 0, len(os))
for n, _ := range os {

View File

View File

@ -91,7 +91,7 @@ func wrappedMain() int {
// just show the version.
args := os.Args[1:]
for _, arg := range args {
if arg == "-v" || arg == "--version" {
if arg == "-v" || arg == "-version" || arg == "--version" {
newArgs := make([]string, len(args)+1)
newArgs[0] = "version"
copy(newArgs[1:], args)

View File

@ -127,6 +127,11 @@ func (c *Context) Apply() (*State, error) {
return c.state, err
}
// Graph returns the graph for this context.
func (c *Context) Graph() (*depgraph.Graph, error) {
return c.graph()
}
// Plan generates an execution plan for the given context.
//
// The execution plan encapsulates the context and can be stored

View File

@ -7,6 +7,28 @@ import (
"testing"
)
func TestContextGraph(t *testing.T) {
p := testProvider("aws")
config := testConfig(t, "validate-good")
c := testContext(t, &ContextOpts{
Config: config,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
})
g, err := c.Graph()
if err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testContextGraph)
if actual != expected {
t.Fatalf("bad: %s", actual)
}
}
func TestContextValidate(t *testing.T) {
p := testProvider("aws")
config := testConfig(t, "validate-good")
@ -1605,3 +1627,15 @@ func testProvisioner() *MockResourceProvisioner {
p := new(MockResourceProvisioner)
return p
}
const testContextGraph = `
root: root
aws_instance.bar
aws_instance.bar -> provider.aws
aws_instance.foo
aws_instance.foo -> provider.aws
provider.aws
root
root -> aws_instance.bar
root -> aws_instance.foo
`

View File

@ -82,15 +82,31 @@ func (p *Plan) init() {
// The format byte is prefixed into the plan file format so that we have
// the ability in the future to change the file format if we want for any
// reason.
const planFormatByte byte = 1
const planFormatMagic = "tfplan"
const planFormatVersion byte = 1
// ReadPlan reads a plan structure out of a reader in the format that
// was written by WritePlan.
func ReadPlan(src io.Reader) (*Plan, error) {
var result *Plan
var err error
n := 0
// Verify the magic bytes
magic := make([]byte, len(planFormatMagic))
for n < len(magic) {
n, err = src.Read(magic[n:])
if err != nil {
return nil, fmt.Errorf("error while reading magic bytes: %s", err)
}
}
if string(magic) != planFormatMagic {
return nil, fmt.Errorf("not a valid plan file")
}
// Verify the version is something we can read
var formatByte [1]byte
n, err := src.Read(formatByte[:])
n, err = src.Read(formatByte[:])
if err != nil {
return nil, err
}
@ -98,7 +114,7 @@ func ReadPlan(src io.Reader) (*Plan, error) {
return nil, errors.New("failed to read plan version byte")
}
if formatByte[0] != planFormatByte {
if formatByte[0] != planFormatVersion {
return nil, fmt.Errorf("unknown plan file version: %d", formatByte[0])
}
@ -112,7 +128,17 @@ func ReadPlan(src io.Reader) (*Plan, error) {
// WritePlan writes a plan somewhere in a binary format.
func WritePlan(d *Plan, dst io.Writer) error {
n, err := dst.Write([]byte{planFormatByte})
// Write the magic bytes so we can determine the file format later
n, err := dst.Write([]byte(planFormatMagic))
if err != nil {
return err
}
if n != len(planFormatMagic) {
return errors.New("failed to write plan format magic bytes")
}
// Write a version byte so we can iterate on version at some point
n, err = dst.Write([]byte{planFormatVersion})
if err != nil {
return err
}

View File

@ -149,15 +149,31 @@ func (s *sensitiveState) init() {
// The format byte is prefixed into the state file format so that we have
// the ability in the future to change the file format if we want for any
// reason.
const stateFormatByte byte = 1
const stateFormatMagic = "tfstate"
const stateFormatVersion byte = 1
// ReadState reads a state structure out of a reader in the format that
// was written by WriteState.
func ReadState(src io.Reader) (*State, error) {
var result *State
var err error
n := 0
// Verify the magic bytes
magic := make([]byte, len(stateFormatMagic))
for n < len(magic) {
n, err = src.Read(magic[n:])
if err != nil {
return nil, fmt.Errorf("error while reading magic bytes: %s", err)
}
}
if string(magic) != stateFormatMagic {
return nil, fmt.Errorf("not a valid state file")
}
// Verify the version is something we can read
var formatByte [1]byte
n, err := src.Read(formatByte[:])
n, err = src.Read(formatByte[:])
if err != nil {
return nil, err
}
@ -165,10 +181,11 @@ func ReadState(src io.Reader) (*State, error) {
return nil, errors.New("failed to read state version byte")
}
if formatByte[0] != stateFormatByte {
if formatByte[0] != stateFormatVersion {
return nil, fmt.Errorf("unknown state file version: %d", formatByte[0])
}
// Decode
dec := gob.NewDecoder(src)
if err := dec.Decode(&result); err != nil {
return nil, err
@ -179,7 +196,17 @@ func ReadState(src io.Reader) (*State, error) {
// WriteState writes a state somewhere in a binary format.
func WriteState(d *State, dst io.Writer) error {
n, err := dst.Write([]byte{stateFormatByte})
// Write the magic bytes so we can determine the file format later
n, err := dst.Write([]byte(stateFormatMagic))
if err != nil {
return err
}
if n != len(stateFormatMagic) {
return errors.New("failed to write state format magic bytes")
}
// Write a version byte so we can iterate on version at some point
n, err = dst.Write([]byte{stateFormatVersion})
if err != nil {
return err
}