diff --git a/command/apply.go b/command/apply.go index 8ecfde398..f900cb019 100644 --- a/command/apply.go +++ b/command/apply.go @@ -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 } diff --git a/command/apply_test.go b/command/apply_test.go index 35d5e34d3..786e3c47b 100644 --- a/command/apply_test.go +++ b/command/apply_test.go @@ -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 { diff --git a/command/command_test.go b/command/command_test.go index 1b87d2bf1..efcefd55b 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -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 +} diff --git a/terraform/plan.go b/terraform/plan.go index f2ecb123d..2325618b8 100644 --- a/terraform/plan.go +++ b/terraform/plan.go @@ -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() diff --git a/terraform/terraform.go b/terraform/terraform.go index c6e426881..cbc2149c6 100644 --- a/terraform/terraform.go +++ b/terraform/terraform.go @@ -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