Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ctrl-C #11

Merged
merged 3 commits into from
Jul 3, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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