Merge pull request #16816 from hashicorp/jbardin/plan-shutdown

Make plan command cancellable
This commit is contained in:
James Bardin 2017-12-01 17:04:31 -05:00 committed by GitHub
commit 90e986348a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 117 additions and 41 deletions

View File

@ -24,9 +24,6 @@ type ApplyCommand struct {
// If true, then this apply command will become the "destroy" // If true, then this apply command will become the "destroy"
// command. It is just like apply but only processes a destroy. // command. It is just like apply but only processes a destroy.
Destroy bool Destroy bool
// When this channel is closed, the apply will be cancelled.
ShutdownCh <-chan struct{}
} }
func (c *ApplyCommand) Run(args []string) int { func (c *ApplyCommand) Run(args []string) int {
@ -186,6 +183,11 @@ func (c *ApplyCommand) Run(args []string) int {
// Cancel our context so we can start gracefully exiting // Cancel our context so we can start gracefully exiting
ctxCancel() ctxCancel()
// notify tests that the command context was canceled
if testShutdownHook != nil {
testShutdownHook()
}
// Notify the user // Notify the user
c.Ui.Output(outputInterrupt) c.Ui.Output(outputInterrupt)

View File

@ -824,22 +824,27 @@ func TestApply_refresh(t *testing.T) {
} }
func TestApply_shutdown(t *testing.T) { func TestApply_shutdown(t *testing.T) {
stopped := false cancelled := false
stopCh := make(chan struct{}) cancelDone := make(chan struct{})
stopReplyCh := make(chan struct{}) testShutdownHook = func() {
cancelled = true
close(cancelDone)
}
defer func() {
testShutdownHook = nil
}()
statePath := testTempFile(t) statePath := testTempFile(t)
p := testProvider() p := testProvider()
shutdownCh := make(chan struct{}) shutdownCh := make(chan struct{})
ui := new(cli.MockUi) ui := new(cli.MockUi)
c := &ApplyCommand{ c := &ApplyCommand{
Meta: Meta{ Meta: Meta{
testingOverrides: metaOverridesForProvider(p), testingOverrides: metaOverridesForProvider(p),
Ui: ui, Ui: ui,
ShutdownCh: shutdownCh,
}, },
ShutdownCh: shutdownCh,
} }
p.DiffFn = func( p.DiffFn = func(
@ -858,10 +863,10 @@ func TestApply_shutdown(t *testing.T) {
*terraform.InstanceInfo, *terraform.InstanceInfo,
*terraform.InstanceState, *terraform.InstanceState,
*terraform.InstanceDiff) (*terraform.InstanceState, error) { *terraform.InstanceDiff) (*terraform.InstanceState, error) {
if !stopped {
stopped = true if !cancelled {
close(stopCh) shutdownCh <- struct{}{}
<-stopReplyCh <-cancelDone
} }
return &terraform.InstanceState{ return &terraform.InstanceState{
@ -872,18 +877,6 @@ func TestApply_shutdown(t *testing.T) {
}, nil }, nil
} }
go func() {
<-stopCh
shutdownCh <- struct{}{}
// This is really dirty, but we have no other way to assure that
// tf.Stop() has been called. This doesn't assure it either, but
// it makes it much more certain.
time.Sleep(50 * time.Millisecond)
close(stopReplyCh)
}()
args := []string{ args := []string{
"-state", statePath, "-state", statePath,
"-auto-approve", "-auto-approve",
@ -897,6 +890,10 @@ func TestApply_shutdown(t *testing.T) {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
if !cancelled {
t.Fatal("command not cancelled")
}
state := testStateRead(t, statePath) state := testStateRead(t, statePath)
if state == nil { if state == nil {
t.Fatal("state should not be nil") t.Fatal("state should not be nil")

View File

@ -17,9 +17,6 @@ import (
// configuration and actually builds or changes infrastructure. // configuration and actually builds or changes infrastructure.
type ConsoleCommand struct { type ConsoleCommand struct {
Meta Meta
// When this channel is closed, the apply will be cancelled.
ShutdownCh <-chan struct{}
} }
func (c *ConsoleCommand) Run(args []string) int { func (c *ConsoleCommand) Run(args []string) int {

View File

@ -76,6 +76,9 @@ type Meta struct {
// is not suitable, e.g. because of a read-only filesystem. // is not suitable, e.g. because of a read-only filesystem.
OverrideDataDir string OverrideDataDir string
// When this channel is closed, the command will be cancelled.
ShutdownCh <-chan struct{}
//---------------------------------------------------------- //----------------------------------------------------------
// Protected: commands can set these // Protected: commands can set these
//---------------------------------------------------------- //----------------------------------------------------------
@ -638,3 +641,7 @@ func isAutoVarFile(path string) bool {
return strings.HasSuffix(path, ".auto.tfvars") || return strings.HasSuffix(path, ".auto.tfvars") ||
strings.HasSuffix(path, ".auto.tfvars.json") strings.HasSuffix(path, ".auto.tfvars.json")
} }
// testShutdownHook is used by tests to verify that a command context has been
// canceled
var testShutdownHook func()

View File

@ -104,17 +104,41 @@ func (c *PlanCommand) Run(args []string) int {
opReq.Type = backend.OperationTypePlan opReq.Type = backend.OperationTypePlan
// Perform the operation // Perform the operation
op, err := b.Operation(context.Background(), opReq) ctx, ctxCancel := context.WithCancel(context.Background())
defer ctxCancel()
op, err := b.Operation(ctx, opReq)
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf("Error starting operation: %s", err)) c.Ui.Error(fmt.Sprintf("Error starting operation: %s", err))
return 1 return 1
} }
// Wait for the operation to complete select {
<-op.Done() case <-c.ShutdownCh:
if err := op.Err; err != nil { // Cancel our context so we can start gracefully exiting
c.showDiagnostics(err) ctxCancel()
return 1
// notify tests that the command context was canceled
if testShutdownHook != nil {
testShutdownHook()
}
// Notify the user
c.Ui.Output(outputInterrupt)
// Still get the result, since there is still one
select {
case <-c.ShutdownCh:
c.Ui.Error(
"Two interrupts received. Exiting immediately")
return 1
case <-op.Done():
}
case <-op.Done():
if err := op.Err; err != nil {
c.showDiagnostics(err)
return 1
}
} }
/* /*

View File

@ -831,6 +831,56 @@ func TestPlan_detailedExitcode_emptyDiff(t *testing.T) {
} }
} }
func TestPlan_shutdown(t *testing.T) {
cancelled := false
cancelDone := make(chan struct{})
testShutdownHook = func() {
cancelled = true
close(cancelDone)
}
defer func() {
testShutdownHook = nil
}()
shutdownCh := make(chan struct{})
p := testProvider()
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
ShutdownCh: shutdownCh,
},
}
p.DiffFn = func(
*terraform.InstanceInfo,
*terraform.InstanceState,
*terraform.ResourceConfig) (*terraform.InstanceDiff, error) {
if !cancelled {
shutdownCh <- struct{}{}
<-cancelDone
}
return &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ami": &terraform.ResourceAttrDiff{
New: "bar",
},
},
}, nil
}
if code := c.Run([]string{testFixturePath("apply-shutdown")}); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
if !cancelled {
t.Fatal("command not cancelled")
}
}
const planVarFile = ` const planVarFile = `
foo = "bar" foo = "bar"
` `

View File

@ -63,6 +63,8 @@ func initCommands(config *Config) {
RunningInAutomation: inAutomation, RunningInAutomation: inAutomation,
PluginCacheDir: config.PluginCacheDir, PluginCacheDir: config.PluginCacheDir,
OverrideDataDir: dataDir, OverrideDataDir: dataDir,
ShutdownCh: makeShutdownCh(),
} }
// The command list is included in the terraform -help // The command list is included in the terraform -help
@ -80,23 +82,20 @@ func initCommands(config *Config) {
Commands = map[string]cli.CommandFactory{ Commands = map[string]cli.CommandFactory{
"apply": func() (cli.Command, error) { "apply": func() (cli.Command, error) {
return &command.ApplyCommand{ return &command.ApplyCommand{
Meta: meta, Meta: meta,
ShutdownCh: makeShutdownCh(),
}, nil }, nil
}, },
"console": func() (cli.Command, error) { "console": func() (cli.Command, error) {
return &command.ConsoleCommand{ return &command.ConsoleCommand{
Meta: meta, Meta: meta,
ShutdownCh: makeShutdownCh(),
}, nil }, nil
}, },
"destroy": func() (cli.Command, error) { "destroy": func() (cli.Command, error) {
return &command.ApplyCommand{ return &command.ApplyCommand{
Meta: meta, Meta: meta,
Destroy: true, Destroy: true,
ShutdownCh: makeShutdownCh(),
}, nil }, nil
}, },