From ab9dd71bcb8e13f1a236b2725b920c0527b05d6f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Sep 2014 21:49:24 -0700 Subject: [PATCH] command/destroy: first steps --- command/apply.go | 56 +++++++++++----- command/apply_destroy_test.go | 120 ++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 17 deletions(-) create mode 100644 command/apply_destroy_test.go diff --git a/command/apply.go b/command/apply.go index 780becf5c..7c048875a 100644 --- a/command/apply.go +++ b/command/apply.go @@ -17,6 +17,11 @@ import ( type ApplyCommand struct { Meta + // If true, then this apply command will become the "destroy" + // command. It is just like apply but only processes a destroy. + Destroy bool + + // When this channel is closed, the apply will be cancelled. ShutdownCh <-chan struct{} } @@ -26,7 +31,12 @@ func (c *ApplyCommand) Run(args []string) int { args = c.Meta.process(args, true) - cmdFlags := c.Meta.flagSet("apply") + cmdName := "apply" + if c.Destroy { + cmdName = "destroy" + } + + cmdFlags := c.Meta.flagSet(cmdName) cmdFlags.BoolVar(&refresh, "refresh", true, "refresh") cmdFlags.StringVar(&statePath, "state", DefaultStateFilename, "path") cmdFlags.StringVar(&stateOutPath, "state-out", "", "path") @@ -70,22 +80,24 @@ func (c *ApplyCommand) Run(args []string) int { backupPath = stateOutPath + DefaultBackupExtention } - // Do a detect to determine if we need to do an init + apply. - if detected, err := module.Detect(configPath, pwd); err != nil { - c.Ui.Error(fmt.Sprintf( - "Invalid path: %s", err)) - return 1 - } else if !strings.HasPrefix(detected, "file") { - // If this isn't a file URL then we're doing an init + - // apply. - var init InitCommand - init.Meta = c.Meta - if code := init.Run([]string{detected}); code != 0 { - return code - } + if !c.Destroy { + // Do a detect to determine if we need to do an init + apply. + if detected, err := module.Detect(configPath, pwd); err != nil { + c.Ui.Error(fmt.Sprintf( + "Invalid path: %s", err)) + return 1 + } else if !strings.HasPrefix(detected, "file") { + // If this isn't a file URL then we're doing an init + + // apply. + var init InitCommand + init.Meta = c.Meta + if code := init.Run([]string{detected}); code != 0 { + return code + } - // Change the config path to be the cwd - configPath = pwd + // Change the config path to be the cwd + configPath = pwd + } } // Build the context based on the arguments given @@ -97,6 +109,11 @@ func (c *ApplyCommand) Run(args []string) int { c.Ui.Error(err.Error()) return 1 } + if planned { + c.Ui.Error(fmt.Sprintf( + "Destroy can't be called with a plan file.")) + return 1 + } if c.Input() { if err := ctx.Input(); err != nil { c.Ui.Error(fmt.Sprintf("Error configuring: %s", err)) @@ -130,7 +147,12 @@ func (c *ApplyCommand) Run(args []string) int { } } - if _, err := ctx.Plan(nil); err != nil { + var opts terraform.PlanOpts + if c.Destroy { + opts.Destroy = true + } + + if _, err := ctx.Plan(&opts); err != nil { c.Ui.Error(fmt.Sprintf( "Error creating plan: %s", err)) return 1 diff --git a/command/apply_destroy_test.go b/command/apply_destroy_test.go new file mode 100644 index 000000000..bde8c888c --- /dev/null +++ b/command/apply_destroy_test.go @@ -0,0 +1,120 @@ +package command + +import ( + "os" + "strings" + "testing" + + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/cli" +) + +func TestApply_destroy(t *testing.T) { + originalState := &terraform.State{ + Modules: []*terraform.ModuleState{ + &terraform.ModuleState{ + Path: []string{"root"}, + Resources: map[string]*terraform.ResourceState{ + "test_instance.foo": &terraform.ResourceState{ + Type: "test_instance", + Primary: &terraform.InstanceState{ + ID: "bar", + }, + }, + }, + }, + }, + } + + statePath := testStateFile(t, originalState) + + p := testProvider() + ui := new(cli.MockUi) + c := &ApplyCommand{ + Destroy: true, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + // Run the apply command pointing to our existing state + args := []string{ + "-state", statePath, + testFixturePath("apply"), + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + // Verify a new state exists + 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") + } + + actualStr := strings.TrimSpace(state.String()) + expectedStr := strings.TrimSpace(testApplyDestroyStr) + if actualStr != expectedStr { + t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr) + } + + // Should have a backup file + f, err = os.Open(statePath + DefaultBackupExtention) + if err != nil { + t.Fatalf("err: %s", err) + } + + backupState, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + actualStr = strings.TrimSpace(backupState.String()) + expectedStr = strings.TrimSpace(originalState.String()) + if actualStr != expectedStr { + t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr) + } +} + +func TestApply_destroyPlan(t *testing.T) { + planPath := testPlanFile(t, &terraform.Plan{ + Module: testModule(t, "apply"), + }) + + p := testProvider() + ui := new(cli.MockUi) + c := &ApplyCommand{ + Destroy: true, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + // Run the apply command pointing to our existing state + args := []string{ + planPath, + } + if code := c.Run(args); code != 1 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } +} + +const testApplyDestroyStr = ` + +`