command/apply: can take a plan as an argument

This commit is contained in:
Mitchell Hashimoto 2014-06-27 14:43:23 -07:00
parent 36ccb3408d
commit fe79e5df03
5 changed files with 179 additions and 98 deletions

View File

@ -20,72 +20,31 @@ type ApplyCommand struct {
func (c *ApplyCommand) Run(args []string) int { func (c *ApplyCommand) Run(args []string) int {
var init bool var init bool
var statePath, stateOutPath string var stateOutPath string
cmdFlags := flag.NewFlagSet("apply", flag.ContinueOnError) cmdFlags := flag.NewFlagSet("apply", flag.ContinueOnError)
cmdFlags.BoolVar(&init, "init", false, "init") cmdFlags.BoolVar(&init, "init", false, "init")
cmdFlags.StringVar(&statePath, "state", "terraform.tfstate", "path") cmdFlags.StringVar(&stateOutPath, "out", "", "path")
cmdFlags.StringVar(&stateOutPath, "state-out", "", "path")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil { if err := cmdFlags.Parse(args); err != nil {
return 1 return 1
} }
args = cmdFlags.Args() args = cmdFlags.Args()
if len(args) != 1 { if len(args) != 2 {
c.Ui.Error( c.Ui.Error("The apply command expects two arguments.\n")
"The apply command expects only one argument with the path\n" +
"to a Terraform configuration.\n")
cmdFlags.Usage() cmdFlags.Usage()
return 1 return 1
} }
if statePath == "" { statePath := args[0]
c.Ui.Error("-state cannot be blank") configPath := args[1]
return 1
}
if stateOutPath == "" { if stateOutPath == "" {
stateOutPath = statePath stateOutPath = statePath
} }
if !init {
if _, err := os.Stat(statePath); err != nil {
c.Ui.Error(fmt.Sprintf(
"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))
return 1
}
}
// Load up the state
var state *terraform.State
if !init {
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
}
}
b, err := config.Load(args[0])
if err != nil {
c.Ui.Error(fmt.Sprintf("Error loading blueprint: %s", err))
return 1
}
// Initialize Terraform right away
c.TFConfig.Hooks = append(c.TFConfig.Hooks, &UiHook{Ui: c.Ui}) c.TFConfig.Hooks = append(c.TFConfig.Hooks, &UiHook{Ui: c.Ui})
tf, err := terraform.New(c.TFConfig) tf, err := terraform.New(c.TFConfig)
if err != nil { if err != nil {
@ -93,27 +52,43 @@ func (c *ApplyCommand) Run(args []string) int {
return 1 return 1
} }
plan, err := tf.Plan(b, state, nil) // Attempt to read a plan from the path given. This is how we test that
if err != nil { // it is a plan or not (kind of jank, but if it quacks like a duck...)
c.Ui.Error(fmt.Sprintf("Error running plan: %s", err)) var plan *terraform.Plan
return 1 f, err := os.Open(configPath)
if err == nil {
plan, err = terraform.ReadPlan(f)
f.Close()
if err != nil {
// Make sure the plan is nil so that we try to load as
// configuration.
plan = nil
}
} }
state, err = tf.Apply(plan) if plan == nil {
// No plan was given, so we're loading from configuration. Generate
// the plan given the configuration.
plan, err = c.configToPlan(tf, init, statePath, configPath)
if err != nil {
c.Ui.Error(err.Error())
return 1
}
}
state, err := tf.Apply(plan)
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf("Error applying plan: %s", err)) c.Ui.Error(fmt.Sprintf("Error applying plan: %s", err))
return 1 return 1
} }
// Write state out to the file // Write state out to the file
f, err := os.Create(stateOutPath) f, err = os.Create(stateOutPath)
if err != nil { if err == nil {
c.Ui.Error(fmt.Sprintf("Failed to save state: %s", err)) err = terraform.WriteState(state, f)
return 1 f.Close()
} }
defer f.Close() if err != nil {
if err := terraform.WriteState(state, f); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to save state: %s", err)) c.Ui.Error(fmt.Sprintf("Failed to save state: %s", err))
return 1 return 1
} }
@ -125,7 +100,7 @@ func (c *ApplyCommand) Run(args []string) int {
func (c *ApplyCommand) Help() string { func (c *ApplyCommand) Help() string {
helpText := ` helpText := `
Usage: terraform apply [terraform.tf] Usage: terraform apply [options] STATE PATH
Builds or changes infrastructure according to the Terraform configuration Builds or changes infrastructure according to the Terraform configuration
file. file.
@ -135,17 +110,60 @@ Options:
-init If specified, it is okay to build brand new -init If specified, it is okay to build brand new
infrastructure (with no state file specified). infrastructure (with no state file specified).
-state=terraform.tfstate Path to the state file to build off of. This file -out=file.tfstate Path to save the new state. If not specified, the
will also be written to with updated state unless state path argument will be used.
-state-out is specified.
-state-out=file.tfstate Path to save the new state. If not specified, the
-state value will be used.
` `
return strings.TrimSpace(helpText) return strings.TrimSpace(helpText)
} }
func (c *ApplyCommand) Synopsis() string { func (c *ApplyCommand) Synopsis() string {
return "Builds or changes infrastructure according to Terrafiles" return "Builds or changes infrastructure"
}
func (c *ApplyCommand) configToPlan(
tf *terraform.Terraform,
init bool,
statePath string,
configPath string) (*terraform.Plan, error) {
if !init {
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 !init {
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(configPath)
if err != nil {
return nil, fmt.Errorf("Error loading config: %s", err)
}
plan, err := tf.Plan(config, state, nil)
if err != nil {
return nil, fmt.Errorf("Error running plan: %s", err)
}
return plan, nil
} }

View File

@ -1,7 +1,6 @@
package command package command
import ( import (
"io/ioutil"
"os" "os"
"reflect" "reflect"
"testing" "testing"
@ -11,13 +10,7 @@ import (
) )
func TestApply(t *testing.T) { func TestApply(t *testing.T) {
tf, err := ioutil.TempFile("", "tf") statePath := testTempFile(t)
if err != nil {
t.Fatalf("err: %s", err)
}
statePath := tf.Name()
tf.Close()
os.Remove(tf.Name())
p := testProvider() p := testProvider()
ui := new(cli.MockUi) ui := new(cli.MockUi)
@ -28,7 +21,7 @@ func TestApply(t *testing.T) {
args := []string{ args := []string{
"-init", "-init",
"-state", statePath, statePath,
testFixturePath("apply"), testFixturePath("apply"),
} }
if code := c.Run(args); code != 0 { if code := c.Run(args); code != 0 {
@ -54,7 +47,10 @@ func TestApply(t *testing.T) {
} }
} }
func TestApply_noState(t *testing.T) { func TestApply_plan(t *testing.T) {
planPath := testPlanFile(t, new(terraform.Plan))
statePath := testTempFile(t)
p := testProvider() p := testProvider()
ui := new(cli.MockUi) ui := new(cli.MockUi)
c := &ApplyCommand{ c := &ApplyCommand{
@ -63,23 +59,33 @@ func TestApply_noState(t *testing.T) {
} }
args := []string{ args := []string{
"-state=", statePath,
testFixturePath("apply"), planPath,
} }
if code := c.Run(args); code != 1 { if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.OutputWriter.String()) 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_state(t *testing.T) { func TestApply_state(t *testing.T) {
// Write out some prior state
tf, err := ioutil.TempFile("", "tf")
if err != nil {
t.Fatalf("err: %s", err)
}
statePath := tf.Name()
defer os.Remove(tf.Name())
originalState := &terraform.State{ originalState := &terraform.State{
Resources: map[string]*terraform.ResourceState{ Resources: map[string]*terraform.ResourceState{
"test_instance.foo": &terraform.ResourceState{ "test_instance.foo": &terraform.ResourceState{
@ -89,11 +95,7 @@ func TestApply_state(t *testing.T) {
}, },
} }
err = terraform.WriteState(originalState, tf) statePath := testStateFile(t, originalState)
tf.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
p := testProvider() p := testProvider()
ui := new(cli.MockUi) ui := new(cli.MockUi)
@ -104,7 +106,7 @@ func TestApply_state(t *testing.T) {
// Run the apply command pointing to our existing state // Run the apply command pointing to our existing state
args := []string{ args := []string{
"-state", statePath, statePath,
testFixturePath("apply"), testFixturePath("apply"),
} }
if code := c.Run(args); code != 0 { if code := c.Run(args); code != 0 {
@ -150,7 +152,7 @@ func TestApply_stateNoExist(t *testing.T) {
} }
args := []string{ args := []string{
"-state=idontexist.tfstate", "idontexist.tfstate",
testFixturePath("apply"), testFixturePath("apply"),
} }
if code := c.Run(args); code != 1 { if code := c.Run(args); code != 1 {

View File

@ -1,7 +1,10 @@
package command package command
import ( import (
"io/ioutil"
"os"
"path/filepath" "path/filepath"
"testing"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
@ -23,6 +26,38 @@ func testTFConfig(p terraform.ResourceProvider) *terraform.Config {
} }
} }
func testPlanFile(t *testing.T, plan *terraform.Plan) string {
path := testTempFile(t)
f, err := os.Create(path)
if err != nil {
t.Fatalf("err: %s", err)
}
defer f.Close()
if err := terraform.WritePlan(plan, f); err != nil {
t.Fatalf("err: %s", err)
}
return path
}
func testStateFile(t *testing.T, s *terraform.State) string {
path := testTempFile(t)
f, err := os.Create(path)
if err != nil {
t.Fatalf("err: %s", err)
}
defer f.Close()
if err := terraform.WriteState(s, f); err != nil {
t.Fatalf("err: %s", err)
}
return path
}
func testProvider() *terraform.MockResourceProvider { func testProvider() *terraform.MockResourceProvider {
p := new(terraform.MockResourceProvider) p := new(terraform.MockResourceProvider)
p.RefreshFn = func( p.RefreshFn = func(
@ -37,3 +72,21 @@ func testProvider() *terraform.MockResourceProvider {
return p return p
} }
func testTempFile(t *testing.T) string {
tf, err := ioutil.TempFile("", "tf")
if err != nil {
t.Fatalf("err: %s", err)
}
result := tf.Name()
if err := tf.Close(); err != nil {
t.Fatalf("err: %s", err)
}
if err := os.Remove(result); err != nil {
t.Fatalf("err: %s", err)
}
return result
}

View File

@ -33,6 +33,10 @@ func (p *Plan) String() string {
func (p *Plan) init() { func (p *Plan) init() {
p.once.Do(func() { p.once.Do(func() {
if p.Config == nil {
p.Config = new(config.Config)
}
if p.Diff == nil { if p.Diff == nil {
p.Diff = new(Diff) p.Diff = new(Diff)
p.Diff.init() p.Diff.init()

View File

@ -42,6 +42,10 @@ func New(c *Config) (*Terraform, error) {
} }
func (t *Terraform) Apply(p *Plan) (*State, error) { func (t *Terraform) Apply(p *Plan) (*State, error) {
// Make sure we're working with a plan that doesn't have null pointers
// everywhere, and is instead just empty otherwise.
p.init()
g, err := t.Graph(p.Config, p.State) g, err := t.Graph(p.Config, p.State)
if err != nil { if err != nil {
return nil, err return nil, err