commit
af482381aa
|
@ -1,6 +1,7 @@
|
|||
*.dll
|
||||
*.exe
|
||||
example.tf
|
||||
terraform.tfplan
|
||||
terraform.tfstate
|
||||
bin/
|
||||
vendor/
|
||||
|
|
|
@ -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
|
||||
|
|
106
command/apply.go
106
command/apply.go
|
@ -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 {
|
||||
ShutdownCh <-chan struct{}
|
||||
ContextOpts *terraform.ContextOpts
|
||||
Ui cli.Ui
|
||||
Meta
|
||||
|
||||
ShutdownCh <-chan struct{}
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
|
@ -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{
|
||||
ContextOpts: testCtxConfig(p),
|
||||
Ui: ui,
|
||||
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{
|
||||
ContextOpts: testCtxConfig(p),
|
||||
Ui: ui,
|
||||
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{
|
||||
ContextOpts: testCtxConfig(p),
|
||||
Ui: ui,
|
||||
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{
|
||||
ContextOpts: testCtxConfig(p),
|
||||
Ui: ui,
|
||||
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{
|
||||
ContextOpts: testCtxConfig(p),
|
||||
ShutdownCh: shutdownCh,
|
||||
Ui: ui,
|
||||
Meta: Meta{
|
||||
ContextOpts: testCtxConfig(p),
|
||||
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{
|
||||
ContextOpts: testCtxConfig(p),
|
||||
Ui: ui,
|
||||
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{
|
||||
ContextOpts: testCtxConfig(p),
|
||||
Ui: ui,
|
||||
Meta: Meta{
|
||||
ContextOpts: testCtxConfig(p),
|
||||
Ui: ui,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func TestColorizeUi_impl(t *testing.T) {
|
||||
var _ cli.Ui = new(ColorizeUi)
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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())
|
||||
}
|
|
@ -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()))
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -1,17 +1,22 @@
|
|||
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
|
||||
|
||||
Ui cli.Ui
|
||||
Colorize *colorstring.Colorize
|
||||
Ui cli.Ui
|
||||
|
||||
once sync.Once
|
||||
ui cli.Ui
|
||||
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
111
command/plan.go
111
command/plan.go
|
@ -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
|
||||
}
|
||||
|
||||
state, err = terraform.ReadState(f)
|
||||
f.Close()
|
||||
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 = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
`
|
||||
|
|
|
@ -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{
|
||||
ContextOpts: testCtxConfig(p),
|
||||
Ui: ui,
|
||||
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{
|
||||
ContextOpts: testCtxConfig(p),
|
||||
Ui: ui,
|
||||
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{
|
||||
ContextOpts: testCtxConfig(p),
|
||||
Ui: ui,
|
||||
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{
|
||||
ContextOpts: testCtxConfig(p),
|
||||
Ui: ui,
|
||||
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{
|
||||
ContextOpts: testCtxConfig(p),
|
||||
Ui: ui,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
} 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 outPath == "" {
|
||||
outPath = statePath
|
||||
// If we don't specify an output path, default to out normal state
|
||||
// path.
|
||||
if stateOutPath == "" {
|
||||
stateOutPath = statePath
|
||||
}
|
||||
|
||||
// Load up the state
|
||||
f, err := os.Open(statePath)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error loading state: %s", err))
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
state, err := terraform.ReadState(f)
|
||||
f.Close()
|
||||
// Build the context based on the arguments given
|
||||
ctx, err := c.Context(configPath, statePath)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error loading state: %s", err))
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
b, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error loading blueprint: %s", err))
|
||||
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"
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
ContextOpts: testCtxConfig(p),
|
||||
Ui: ui,
|
||||
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{
|
||||
ContextOpts: testCtxConfig(p),
|
||||
Ui: ui,
|
||||
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 {
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
resource "test_instance" "foo" {
|
||||
ami = "bar"
|
||||
}
|
|
@ -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)
|
||||
|
|
33
commands.go
33
commands.go
|
@ -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{
|
||||
ShutdownCh: makeShutdownCh(),
|
||||
ContextOpts: &ContextOpts,
|
||||
Ui: Ui,
|
||||
Meta: meta,
|
||||
ShutdownCh: makeShutdownCh(),
|
||||
}, 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
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
2
main.go
2
main.go
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
`
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue