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 {
var init bool
var statePath, stateOutPath string
var stateOutPath string
cmdFlags := flag.NewFlagSet("apply", flag.ContinueOnError)
cmdFlags.BoolVar(&init, "init", false, "init")
cmdFlags.StringVar(&statePath, "state", "terraform.tfstate", "path")
cmdFlags.StringVar(&stateOutPath, "state-out", "", "path")
cmdFlags.StringVar(&stateOutPath, "out", "", "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 {
c.Ui.Error(
"The apply command expects only one argument with the path\n" +
"to a Terraform configuration.\n")
if len(args) != 2 {
c.Ui.Error("The apply command expects two arguments.\n")
cmdFlags.Usage()
return 1
}
if statePath == "" {
c.Ui.Error("-state cannot be blank")
return 1
}
statePath := args[0]
configPath := args[1]
if stateOutPath == "" {
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})
tf, err := terraform.New(c.TFConfig)
if err != nil {
@ -93,27 +52,43 @@ func (c *ApplyCommand) Run(args []string) int {
return 1
}
plan, err := tf.Plan(b, state, nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error running plan: %s", err))
return 1
// Attempt to read a plan from the path given. This is how we test that
// it is a plan or not (kind of jank, but if it quacks like a duck...)
var plan *terraform.Plan
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 {
c.Ui.Error(fmt.Sprintf("Error applying plan: %s", err))
return 1
}
// Write state out to the file
f, err := os.Create(stateOutPath)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to save state: %s", err))
return 1
f, err = os.Create(stateOutPath)
if err == nil {
err = terraform.WriteState(state, f)
f.Close()
}
defer f.Close()
if err := terraform.WriteState(state, f); err != nil {
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to save state: %s", err))
return 1
}
@ -125,7 +100,7 @@ func (c *ApplyCommand) Run(args []string) int {
func (c *ApplyCommand) Help() string {
helpText := `
Usage: terraform apply [terraform.tf]
Usage: terraform apply [options] STATE PATH
Builds or changes infrastructure according to the Terraform configuration
file.
@ -135,17 +110,60 @@ Options:
-init If specified, it is okay to build brand new
infrastructure (with no state file specified).
-state=terraform.tfstate Path to the state file to build off of. This file
will also be written to with updated state unless
-state-out is specified.
-state-out=file.tfstate Path to save the new state. If not specified, the
-state value will be used.
-out=file.tfstate Path to save the new state. If not specified, the
state path argument will be used.
`
return strings.TrimSpace(helpText)
}
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
import (
"io/ioutil"
"os"
"reflect"
"testing"
@ -11,13 +10,7 @@ import (
)
func TestApply(t *testing.T) {
tf, err := ioutil.TempFile("", "tf")
if err != nil {
t.Fatalf("err: %s", err)
}
statePath := tf.Name()
tf.Close()
os.Remove(tf.Name())
statePath := testTempFile(t)
p := testProvider()
ui := new(cli.MockUi)
@ -28,7 +21,7 @@ func TestApply(t *testing.T) {
args := []string{
"-init",
"-state", statePath,
statePath,
testFixturePath("apply"),
}
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()
ui := new(cli.MockUi)
c := &ApplyCommand{
@ -63,23 +59,33 @@ func TestApply_noState(t *testing.T) {
}
args := []string{
"-state=",
testFixturePath("apply"),
statePath,
planPath,
}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: \n%s", ui.OutputWriter.String())
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_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{
Resources: map[string]*terraform.ResourceState{
"test_instance.foo": &terraform.ResourceState{
@ -89,11 +95,7 @@ func TestApply_state(t *testing.T) {
},
}
err = terraform.WriteState(originalState, tf)
tf.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
statePath := testStateFile(t, originalState)
p := testProvider()
ui := new(cli.MockUi)
@ -104,7 +106,7 @@ func TestApply_state(t *testing.T) {
// Run the apply command pointing to our existing state
args := []string{
"-state", statePath,
statePath,
testFixturePath("apply"),
}
if code := c.Run(args); code != 0 {
@ -150,7 +152,7 @@ func TestApply_stateNoExist(t *testing.T) {
}
args := []string{
"-state=idontexist.tfstate",
"idontexist.tfstate",
testFixturePath("apply"),
}
if code := c.Run(args); code != 1 {

View File

@ -1,7 +1,10 @@
package command
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"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 {
p := new(terraform.MockResourceProvider)
p.RefreshFn = func(
@ -37,3 +72,21 @@ func testProvider() *terraform.MockResourceProvider {
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() {
p.once.Do(func() {
if p.Config == nil {
p.Config = new(config.Config)
}
if p.Diff == nil {
p.Diff = new(Diff)
p.Diff.init()

View File

@ -42,6 +42,10 @@ func New(c *Config) (*Terraform, 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)
if err != nil {
return nil, err