Skip to content

Commit

Permalink
Merge pull request #11 from hashicorp/f-cancel
Browse files Browse the repository at this point in the history
Ctrl-C
  • Loading branch information
mitchellh committed Jul 3, 2014
2 parents 8c330d9 + 5aa6ada commit c510525
Show file tree
Hide file tree
Showing 10 changed files with 428 additions and 18 deletions.
41 changes: 38 additions & 3 deletions command/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import (
// ApplyCommand is a Command implementation that applies a Terraform
// configuration and actually builds or changes infrastructure.
type ApplyCommand struct {
TFConfig *terraform.Config
Ui cli.Ui
ShutdownCh chan struct{}
TFConfig *terraform.Config
Ui cli.Ui
}

func (c *ApplyCommand) Run(args []string) int {
Expand Down Expand Up @@ -63,7 +64,41 @@ func (c *ApplyCommand) Run(args []string) int {
return 1
}

state, err := tf.Apply(plan)
errCh := make(chan error)
stateCh := make(chan *terraform.State)
go func() {
state, err := tf.Apply(plan)
if err != nil {
errCh <- err
return
}

stateCh <- state
}()

err = nil
var state *terraform.State
select {
case <-c.ShutdownCh:
c.Ui.Output("Interrupt received. Gracefully shutting down...")

// Stop execution
tf.Stop()

// Still get the result, since there is still one
select {
case <-c.ShutdownCh:
c.Ui.Error(
"Two interrupts received. Exiting immediately. Note that data\n" +
"loss may have occurred.")
return 1
case state = <-stateCh:
case err = <-errCh:
}
case state = <-stateCh:
case err = <-errCh:
}

if err != nil {
c.Ui.Error(fmt.Sprintf("Error applying plan: %s", err))
return 1
Expand Down
82 changes: 82 additions & 0 deletions command/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,88 @@ func TestApply_plan(t *testing.T) {
}
}

func TestApply_shutdown(t *testing.T) {
stopped := false
stopCh := make(chan struct{})
stopReplyCh := make(chan struct{})

statePath := testTempFile(t)

p := testProvider()
shutdownCh := make(chan struct{})
ui := new(cli.MockUi)
c := &ApplyCommand{
ShutdownCh: shutdownCh,
TFConfig: testTFConfig(p),
Ui: ui,
}

p.DiffFn = func(
*terraform.ResourceState,
*terraform.ResourceConfig) (*terraform.ResourceDiff, error) {
return &terraform.ResourceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ami": &terraform.ResourceAttrDiff{
New: "bar",
},
},
}, nil
}
p.ApplyFn = func(
*terraform.ResourceState,
*terraform.ResourceDiff) (*terraform.ResourceState, error) {
if !stopped {
stopped = true
close(stopCh)
<-stopReplyCh
}

return &terraform.ResourceState{
ID: "foo",
Attributes: map[string]string{
"ami": "2",
},
}, nil
}

go func() {
<-stopCh
shutdownCh <- struct{}{}
close(stopReplyCh)
}()

args := []string{
"-init",
statePath,
testFixturePath("apply-shutdown"),
}
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")
}

if len(state.Resources) != 1 {
t.Fatalf("bad: %d", len(state.Resources))
}
}

func TestApply_state(t *testing.T) {
originalState := &terraform.State{
Resources: map[string]*terraform.ResourceState{
Expand Down
7 changes: 7 additions & 0 deletions command/test-fixtures/apply-shutdown/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
resource "test_instance" "foo" {
ami = "bar"
}

resource "test_instance" "bar" {
ami = "${test_instance.foo.ami}"
}
23 changes: 21 additions & 2 deletions commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"os"
"os/signal"

"github.com/hashicorp/terraform/command"
"github.com/mitchellh/cli"
Expand All @@ -28,8 +29,9 @@ func init() {
Commands = map[string]cli.CommandFactory{
"apply": func() (cli.Command, error) {
return &command.ApplyCommand{
TFConfig: &TFConfig,
Ui: Ui,
ShutdownCh: makeShutdownCh(),
TFConfig: &TFConfig,
Ui: Ui,
}, nil
},

Expand Down Expand Up @@ -64,3 +66,20 @@ func init() {
},
}
}

// makeShutdownCh creates an interrupt listener and returns a channel.
// A message will be sent on the channel for every interrupt received.
func makeShutdownCh() <-chan struct{} {
resultCh := make(chan struct{})

signalCh := make(chan os.Signal, 4)
signal.Notify(signalCh, os.Interrupt)
go func() {
for {
<-signalCh
resultCh <- struct{}{}
}
}()

return resultCh
}
16 changes: 16 additions & 0 deletions terraform/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,19 @@ func (*NilHook) PreRefresh(string, *ResourceState) (HookAction, error) {
func (*NilHook) PostRefresh(string, *ResourceState) (HookAction, error) {
return HookActionContinue, nil
}

// handleHook turns hook actions into panics. This lets you use the
// panic/recover mechanism in Go as a flow control mechanism for hook
// actions.
func handleHook(a HookAction, err error) {
if err != nil {
// TODO: handle errors
}

switch a {
case HookActionContinue:
return
case HookActionHalt:
panic(HookActionHalt)
}
}
79 changes: 79 additions & 0 deletions terraform/hook_stop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package terraform

import (
"sync"
)

// stopHook is a private Hook implementation that Terraform uses to
// signal when to stop or cancel actions.
type stopHook struct {
sync.Mutex

// This should be incremented for every thing that can be stopped.
// When this is zero, a stopper can assume that everything is properly
// stopped.
count int

// This channel should be closed when it is time to stop
ch chan struct{}

serial int
stoppedCh chan<- struct{}
}

func (h *stopHook) PreApply(string, *ResourceState, *ResourceDiff) (HookAction, error) {
return h.hook()
}

func (h *stopHook) PostApply(string, *ResourceState) (HookAction, error) {
return h.hook()
}

func (h *stopHook) PreDiff(string, *ResourceState) (HookAction, error) {
return h.hook()
}

func (h *stopHook) PostDiff(string, *ResourceDiff) (HookAction, error) {
return h.hook()
}

func (h *stopHook) PreRefresh(string, *ResourceState) (HookAction, error) {
return h.hook()
}

func (h *stopHook) PostRefresh(string, *ResourceState) (HookAction, error) {
return h.hook()
}

func (h *stopHook) hook() (HookAction, error) {
select {
case <-h.ch:
h.stoppedCh <- struct{}{}
return HookActionHalt, nil
default:
return HookActionContinue, nil
}
}

// reset should be called within the lock context
func (h *stopHook) reset() {
h.ch = make(chan struct{})
h.count = 0
h.serial += 1
h.stoppedCh = nil
}

func (h *stopHook) ref() int {
h.Lock()
defer h.Unlock()
h.count++
return h.serial
}

func (h *stopHook) unref(s int) {
h.Lock()
defer h.Unlock()
if h.serial == s {
h.count--
}
}
9 changes: 9 additions & 0 deletions terraform/hook_stop_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package terraform

import (
"testing"
)

func TestStopHook_impl(t *testing.T) {
var _ Hook = new(stopHook)
}
Loading

0 comments on commit c510525

Please sign in to comment.