Merge branch 'cancel-next'
This implements more robust interrupt handling by propagating cancels throughout more core Packer components.
This commit is contained in:
commit
3238b65bf9
|
@ -4,6 +4,7 @@ import (
|
|||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StepProvision runs the provisioners.
|
||||
|
@ -22,13 +23,31 @@ func (*StepProvision) Run(state map[string]interface{}) multistep.StepAction {
|
|||
hook := state["hook"].(packer.Hook)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
// Run the provisioner in a goroutine so we can continually check
|
||||
// for cancellations...
|
||||
log.Println("Running the provision hook")
|
||||
if err := hook.Run(packer.HookProvision, ui, comm, nil); err != nil {
|
||||
state["error"] = err
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
errCh <- hook.Run(packer.HookProvision, ui, comm, nil)
|
||||
}()
|
||||
|
||||
return multistep.ActionContinue
|
||||
for {
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err != nil {
|
||||
state["error"] = err
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
case <-time.After(1 * time.Second):
|
||||
if _, ok := state[multistep.StateCancelled]; ok {
|
||||
log.Println("Cancelling provisioning due to interrupt...")
|
||||
hook.Cancel()
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (*StepProvision) Cleanup(map[string]interface{}) {}
|
||||
|
|
|
@ -207,10 +207,12 @@ func (b *coreBuild) Run(originalUi Ui, cache Cache) ([]Artifact, error) {
|
|||
hooks[HookProvision] = make([]Hook, 0, 1)
|
||||
}
|
||||
|
||||
hooks[HookProvision] = append(hooks[HookProvision], &ProvisionHook{provisioners})
|
||||
hooks[HookProvision] = append(hooks[HookProvision], &ProvisionHook{
|
||||
Provisioners: provisioners,
|
||||
})
|
||||
}
|
||||
|
||||
hook := &DispatchHook{hooks}
|
||||
hook := &DispatchHook{Mapping: hooks}
|
||||
artifacts := make([]Artifact, 0, 1)
|
||||
|
||||
// The builder just has a normal Ui, but targetted
|
||||
|
|
|
@ -13,10 +13,10 @@ func testBuild() *coreBuild {
|
|||
builderConfig: 42,
|
||||
builderType: "foo",
|
||||
hooks: map[string][]Hook{
|
||||
"foo": []Hook{&TestHook{}},
|
||||
"foo": []Hook{&MockHook{}},
|
||||
},
|
||||
provisioners: []coreBuildProvisioner{
|
||||
coreBuildProvisioner{&TestProvisioner{}, []interface{}{42}},
|
||||
coreBuildProvisioner{&MockProvisioner{}, []interface{}{42}},
|
||||
},
|
||||
postProcessors: [][]coreBuildPostProcessor{
|
||||
[]coreBuildPostProcessor{
|
||||
|
@ -59,9 +59,9 @@ func TestBuild_Prepare(t *testing.T) {
|
|||
assert.Equal(builder.prepareConfig, []interface{}{42, packerConfig}, "prepare config should be 42")
|
||||
|
||||
coreProv := build.provisioners[0]
|
||||
prov := coreProv.provisioner.(*TestProvisioner)
|
||||
assert.True(prov.prepCalled, "prepare should be called")
|
||||
assert.Equal(prov.prepConfigs, []interface{}{42, packerConfig}, "prepare should be called with proper config")
|
||||
prov := coreProv.provisioner.(*MockProvisioner)
|
||||
assert.True(prov.PrepCalled, "prepare should be called")
|
||||
assert.Equal(prov.PrepConfigs, []interface{}{42, packerConfig}, "prepare should be called with proper config")
|
||||
|
||||
corePP := build.postProcessors[0][0]
|
||||
pp := corePP.processor.(*TestPostProcessor)
|
||||
|
@ -104,9 +104,9 @@ func TestBuild_Prepare_Debug(t *testing.T) {
|
|||
assert.Equal(builder.prepareConfig, []interface{}{42, packerConfig}, "prepare config should be 42")
|
||||
|
||||
coreProv := build.provisioners[0]
|
||||
prov := coreProv.provisioner.(*TestProvisioner)
|
||||
assert.True(prov.prepCalled, "prepare should be called")
|
||||
assert.Equal(prov.prepConfigs, []interface{}{42, packerConfig}, "prepare should be called with proper config")
|
||||
prov := coreProv.provisioner.(*MockProvisioner)
|
||||
assert.True(prov.PrepCalled, "prepare should be called")
|
||||
assert.Equal(prov.PrepConfigs, []interface{}{42, packerConfig}, "prepare should be called with proper config")
|
||||
}
|
||||
|
||||
func TestBuildPrepare_variables_default(t *testing.T) {
|
||||
|
@ -187,14 +187,14 @@ func TestBuild_Run(t *testing.T) {
|
|||
dispatchHook := builder.runHook
|
||||
dispatchHook.Run("foo", nil, nil, 42)
|
||||
|
||||
hook := build.hooks["foo"][0].(*TestHook)
|
||||
assert.True(hook.runCalled, "run should be called")
|
||||
assert.Equal(hook.runData, 42, "should have correct data")
|
||||
hook := build.hooks["foo"][0].(*MockHook)
|
||||
assert.True(hook.RunCalled, "run should be called")
|
||||
assert.Equal(hook.RunData, 42, "should have correct data")
|
||||
|
||||
// Verify provisioners run
|
||||
dispatchHook.Run(HookProvision, nil, nil, 42)
|
||||
prov := build.provisioners[0].provisioner.(*TestProvisioner)
|
||||
assert.True(prov.provCalled, "provision should be called")
|
||||
prov := build.provisioners[0].provisioner.(*MockProvisioner)
|
||||
assert.True(prov.ProvCalled, "provision should be called")
|
||||
|
||||
// Verify post-processor was run
|
||||
pp := build.postProcessors[0][0].processor.(*TestPostProcessor)
|
||||
|
|
|
@ -20,7 +20,7 @@ func init() {
|
|||
func testComponentFinder() *ComponentFinder {
|
||||
builderFactory := func(n string) (Builder, error) { return testBuilder(), nil }
|
||||
ppFactory := func(n string) (PostProcessor, error) { return new(TestPostProcessor), nil }
|
||||
provFactory := func(n string) (Provisioner, error) { return new(TestProvisioner), nil }
|
||||
provFactory := func(n string) (Provisioner, error) { return new(MockProvisioner), nil }
|
||||
return &ComponentFinder{
|
||||
Builder: builderFactory,
|
||||
PostProcessor: ppFactory,
|
||||
|
@ -227,7 +227,7 @@ func TestEnvironment_DefaultCli_Version(t *testing.T) {
|
|||
func TestEnvironment_Hook(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
hook := &TestHook{}
|
||||
hook := &MockHook{}
|
||||
hooks := make(map[string]Hook)
|
||||
hooks["foo"] = hook
|
||||
|
||||
|
@ -309,7 +309,7 @@ func TestEnvironment_PostProcessor_Error(t *testing.T) {
|
|||
func TestEnvironmentProvisioner(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
p := &TestProvisioner{}
|
||||
p := &MockProvisioner{}
|
||||
ps := make(map[string]Provisioner)
|
||||
ps["foo"] = p
|
||||
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
package packer
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// This is the hook that should be fired for provisioners to run.
|
||||
const HookProvision = "packer_provision"
|
||||
|
||||
|
@ -11,19 +15,40 @@ const HookProvision = "packer_provision"
|
|||
// you must reference the documentation for the specific hook you're interested
|
||||
// in. In addition to that, the Hook is given access to a UI so that it can
|
||||
// output things to the user.
|
||||
//
|
||||
// Cancel is called when the hook needs to be cancelled. This will usually
|
||||
// be called when Run is still in progress so the mechanism that handles this
|
||||
// must be race-free. Cancel should attempt to cancel the hook in the
|
||||
// quickest, safest way possible.
|
||||
type Hook interface {
|
||||
Run(string, Ui, Communicator, interface{}) error
|
||||
Cancel()
|
||||
}
|
||||
|
||||
// A Hook implementation that dispatches based on an internal mapping.
|
||||
type DispatchHook struct {
|
||||
Mapping map[string][]Hook
|
||||
|
||||
l sync.Mutex
|
||||
cancelled bool
|
||||
runningHook Hook
|
||||
}
|
||||
|
||||
// Runs the hook with the given name by dispatching it to the proper
|
||||
// hooks if a mapping exists. If a mapping doesn't exist, then nothing
|
||||
// happens.
|
||||
func (h *DispatchHook) Run(name string, ui Ui, comm Communicator, data interface{}) error {
|
||||
h.l.Lock()
|
||||
h.cancelled = false
|
||||
h.l.Unlock()
|
||||
|
||||
// Make sure when we exit that we reset the running hook.
|
||||
defer func() {
|
||||
h.l.Lock()
|
||||
defer h.l.Unlock()
|
||||
h.runningHook = nil
|
||||
}()
|
||||
|
||||
hooks, ok := h.Mapping[name]
|
||||
if !ok {
|
||||
// No hooks for that name. No problem.
|
||||
|
@ -31,6 +56,15 @@ func (h *DispatchHook) Run(name string, ui Ui, comm Communicator, data interface
|
|||
}
|
||||
|
||||
for _, hook := range hooks {
|
||||
h.l.Lock()
|
||||
if h.cancelled {
|
||||
h.l.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
h.runningHook = hook
|
||||
h.l.Unlock()
|
||||
|
||||
if err := hook.Run(name, ui, comm, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -38,3 +72,16 @@ func (h *DispatchHook) Run(name string, ui Ui, comm Communicator, data interface
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cancels all the hooks that are currently in-flight, if any. This will
|
||||
// block until the hooks are all cancelled.
|
||||
func (h *DispatchHook) Cancel() {
|
||||
h.l.Lock()
|
||||
defer h.l.Unlock()
|
||||
|
||||
if h.runningHook != nil {
|
||||
h.runningHook.Cancel()
|
||||
}
|
||||
|
||||
h.cancelled = true
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
package packer
|
||||
|
||||
// MockHook is an implementation of Hook that can be used for tests.
|
||||
type MockHook struct {
|
||||
RunFunc func() error
|
||||
|
||||
RunCalled bool
|
||||
RunComm Communicator
|
||||
RunData interface{}
|
||||
RunName string
|
||||
RunUi Ui
|
||||
CancelCalled bool
|
||||
}
|
||||
|
||||
func (t *MockHook) Run(name string, ui Ui, comm Communicator, data interface{}) error {
|
||||
t.RunCalled = true
|
||||
t.RunComm = comm
|
||||
t.RunData = data
|
||||
t.RunName = name
|
||||
t.RunUi = ui
|
||||
|
||||
if t.RunFunc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return t.RunFunc()
|
||||
}
|
||||
|
||||
func (t *MockHook) Cancel() {
|
||||
t.CancelCalled = true
|
||||
}
|
|
@ -2,52 +2,89 @@ package packer
|
|||
|
||||
import (
|
||||
"cgl.tideland.biz/asserts"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TestHook struct {
|
||||
runCalled bool
|
||||
runComm Communicator
|
||||
runData interface{}
|
||||
runName string
|
||||
runUi Ui
|
||||
// A helper Hook implementation for testing cancels.
|
||||
type CancelHook struct {
|
||||
sync.Mutex
|
||||
cancelCh chan struct{}
|
||||
doneCh chan struct{}
|
||||
|
||||
Cancelled bool
|
||||
}
|
||||
|
||||
func (t *TestHook) Run(name string, ui Ui, comm Communicator, data interface{}) error {
|
||||
t.runCalled = true
|
||||
t.runComm = comm
|
||||
t.runData = data
|
||||
t.runName = name
|
||||
t.runUi = ui
|
||||
func (h *CancelHook) Run(string, Ui, Communicator, interface{}) error {
|
||||
h.Lock()
|
||||
h.cancelCh = make(chan struct{})
|
||||
h.doneCh = make(chan struct{})
|
||||
h.Unlock()
|
||||
|
||||
defer close(h.doneCh)
|
||||
|
||||
select {
|
||||
case <-h.cancelCh:
|
||||
h.Cancelled = true
|
||||
case <-time.After(1 * time.Second):
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *CancelHook) Cancel() {
|
||||
h.Lock()
|
||||
close(h.cancelCh)
|
||||
h.Unlock()
|
||||
|
||||
<-h.doneCh
|
||||
}
|
||||
|
||||
func TestDispatchHook_Implements(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
var r Hook
|
||||
c := &DispatchHook{nil}
|
||||
c := &DispatchHook{}
|
||||
|
||||
assert.Implementor(c, &r, "should be a Hook")
|
||||
}
|
||||
|
||||
func TestDispatchHook_Run_NoHooks(t *testing.T) {
|
||||
// Just make sure nothing blows up
|
||||
dh := &DispatchHook{make(map[string][]Hook)}
|
||||
dh := &DispatchHook{}
|
||||
dh.Run("foo", nil, nil, nil)
|
||||
}
|
||||
|
||||
func TestDispatchHook_Run(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
hook := &TestHook{}
|
||||
hook := &MockHook{}
|
||||
|
||||
mapping := make(map[string][]Hook)
|
||||
mapping["foo"] = []Hook{hook}
|
||||
dh := &DispatchHook{mapping}
|
||||
dh := &DispatchHook{Mapping: mapping}
|
||||
dh.Run("foo", nil, nil, 42)
|
||||
|
||||
assert.True(hook.runCalled, "run should be called")
|
||||
assert.Equal(hook.runName, "foo", "should be proper event")
|
||||
assert.Equal(hook.runData, 42, "should be correct data")
|
||||
assert.True(hook.RunCalled, "run should be called")
|
||||
assert.Equal(hook.RunName, "foo", "should be proper event")
|
||||
assert.Equal(hook.RunData, 42, "should be correct data")
|
||||
}
|
||||
|
||||
func TestDispatchHook_cancel(t *testing.T) {
|
||||
hook := new(CancelHook)
|
||||
|
||||
dh := &DispatchHook{
|
||||
Mapping: map[string][]Hook{
|
||||
"foo": []Hook{hook},
|
||||
},
|
||||
}
|
||||
|
||||
go dh.Run("foo", nil, nil, 42)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
dh.Cancel()
|
||||
|
||||
if !hook.Cancelled {
|
||||
t.Fatal("hook should've cancelled")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,8 +19,17 @@ func (c *cmdHook) Run(name string, ui packer.Ui, comm packer.Communicator, data
|
|||
return c.hook.Run(name, ui, comm, data)
|
||||
}
|
||||
|
||||
func (c *cmdHook) Cancel() {
|
||||
defer func() {
|
||||
r := recover()
|
||||
c.checkExit(r, nil)
|
||||
}()
|
||||
|
||||
c.hook.Cancel()
|
||||
}
|
||||
|
||||
func (c *cmdHook) checkExit(p interface{}, cb func()) {
|
||||
if c.client.Exited() {
|
||||
if c.client.Exited() && cb != nil {
|
||||
cb()
|
||||
} else if p != nil && !Killed {
|
||||
log.Panic(p)
|
||||
|
|
|
@ -1,17 +1,10 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"os/exec"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type helperHook byte
|
||||
|
||||
func (helperHook) Run(string, packer.Ui, packer.Communicator, interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestHook_NoExist(t *testing.T) {
|
||||
c := NewClient(&ClientConfig{Cmd: exec.Command("i-should-not-exist")})
|
||||
defer c.Kill()
|
||||
|
|
|
@ -2,6 +2,7 @@ package plugin
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
@ -54,7 +55,7 @@ func TestHelperProcess(*testing.T) {
|
|||
case "command":
|
||||
ServeCommand(new(helperCommand))
|
||||
case "hook":
|
||||
ServeHook(new(helperHook))
|
||||
ServeHook(new(packer.MockHook))
|
||||
case "invalid-rpc-address":
|
||||
fmt.Println("lolinvalid")
|
||||
case "mock":
|
||||
|
@ -63,7 +64,7 @@ func TestHelperProcess(*testing.T) {
|
|||
case "post-processor":
|
||||
ServePostProcessor(new(helperPostProcessor))
|
||||
case "provisioner":
|
||||
ServeProvisioner(new(helperProvisioner))
|
||||
ServeProvisioner(new(packer.MockProvisioner))
|
||||
case "start-timeout":
|
||||
time.Sleep(1 * time.Minute)
|
||||
os.Exit(1)
|
||||
|
|
|
@ -28,6 +28,15 @@ func (c *cmdProvisioner) Provision(ui packer.Ui, comm packer.Communicator) error
|
|||
return c.p.Provision(ui, comm)
|
||||
}
|
||||
|
||||
func (c *cmdProvisioner) Cancel() {
|
||||
defer func() {
|
||||
r := recover()
|
||||
c.checkExit(r, nil)
|
||||
}()
|
||||
|
||||
c.p.Cancel()
|
||||
}
|
||||
|
||||
func (c *cmdProvisioner) checkExit(p interface{}, cb func()) {
|
||||
if c.client.Exited() && cb != nil {
|
||||
cb()
|
||||
|
|
|
@ -1,21 +1,10 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"os/exec"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type helperProvisioner byte
|
||||
|
||||
func (helperProvisioner) Prepare(...interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (helperProvisioner) Provision(packer.Ui, packer.Communicator) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestProvisioner_NoExist(t *testing.T) {
|
||||
c := NewClient(&ClientConfig{Cmd: exec.Command("i-should-not-exist")})
|
||||
defer c.Kill()
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
package packer
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// A provisioner is responsible for installing and configuring software
|
||||
// on a machine prior to building the actual image.
|
||||
type Provisioner interface {
|
||||
|
@ -13,6 +17,11 @@ type Provisioner interface {
|
|||
// is guaranteed to be connected to some machine so that provisioning
|
||||
// can be done.
|
||||
Provision(Ui, Communicator) error
|
||||
|
||||
// Cancel is called to cancel the provisioning. This is usually called
|
||||
// while Provision is still being called. The Provisioner should act
|
||||
// to stop its execution as quickly as possible in a race-free way.
|
||||
Cancel()
|
||||
}
|
||||
|
||||
// A Hook implementation that runs the given provisioners.
|
||||
|
@ -20,11 +29,25 @@ type ProvisionHook struct {
|
|||
// The provisioners to run as part of the hook. These should already
|
||||
// be prepared (by calling Prepare) at some earlier stage.
|
||||
Provisioners []Provisioner
|
||||
|
||||
lock sync.Mutex
|
||||
runningProvisioner Provisioner
|
||||
}
|
||||
|
||||
// Runs the provisioners in order.
|
||||
func (h *ProvisionHook) Run(name string, ui Ui, comm Communicator, data interface{}) error {
|
||||
defer func() {
|
||||
h.lock.Lock()
|
||||
defer h.lock.Unlock()
|
||||
|
||||
h.runningProvisioner = nil
|
||||
}()
|
||||
|
||||
for _, p := range h.Provisioners {
|
||||
h.lock.Lock()
|
||||
h.runningProvisioner = p
|
||||
h.lock.Unlock()
|
||||
|
||||
if err := p.Provision(ui, comm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -32,3 +55,13 @@ func (h *ProvisionHook) Run(name string, ui Ui, comm Communicator, data interfac
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cancels the privisioners that are still running.
|
||||
func (h *ProvisionHook) Cancel() {
|
||||
h.lock.Lock()
|
||||
defer h.lock.Unlock()
|
||||
|
||||
if h.runningProvisioner != nil {
|
||||
h.runningProvisioner.Cancel()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
package packer
|
||||
|
||||
// MockProvisioner is an implementation of Provisioner that can be
|
||||
// used for tests.
|
||||
type MockProvisioner struct {
|
||||
ProvFunc func() error
|
||||
|
||||
PrepCalled bool
|
||||
PrepConfigs []interface{}
|
||||
ProvCalled bool
|
||||
ProvUi Ui
|
||||
CancelCalled bool
|
||||
}
|
||||
|
||||
func (t *MockProvisioner) Prepare(configs ...interface{}) error {
|
||||
t.PrepCalled = true
|
||||
t.PrepConfigs = configs
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *MockProvisioner) Provision(ui Ui, comm Communicator) error {
|
||||
t.ProvCalled = true
|
||||
t.ProvUi = ui
|
||||
|
||||
if t.ProvFunc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return t.ProvFunc()
|
||||
}
|
||||
|
||||
func (t *MockProvisioner) Cancel() {
|
||||
t.CancelCalled = true
|
||||
}
|
|
@ -1,23 +1,10 @@
|
|||
package packer
|
||||
|
||||
import "testing"
|
||||
|
||||
type TestProvisioner struct {
|
||||
prepCalled bool
|
||||
prepConfigs []interface{}
|
||||
provCalled bool
|
||||
}
|
||||
|
||||
func (t *TestProvisioner) Prepare(configs ...interface{}) error {
|
||||
t.prepCalled = true
|
||||
t.prepConfigs = configs
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TestProvisioner) Provision(Ui, Communicator) error {
|
||||
t.provCalled = true
|
||||
return nil
|
||||
}
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestProvisionHook_Impl(t *testing.T) {
|
||||
var raw interface{}
|
||||
|
@ -28,23 +15,68 @@ func TestProvisionHook_Impl(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestProvisionHook(t *testing.T) {
|
||||
pA := &TestProvisioner{}
|
||||
pB := &TestProvisioner{}
|
||||
pA := &MockProvisioner{}
|
||||
pB := &MockProvisioner{}
|
||||
|
||||
ui := testUi()
|
||||
var comm Communicator = nil
|
||||
var data interface{} = nil
|
||||
|
||||
hook := &ProvisionHook{[]Provisioner{pA, pB}}
|
||||
hook := &ProvisionHook{
|
||||
Provisioners: []Provisioner{pA, pB},
|
||||
}
|
||||
|
||||
hook.Run("foo", ui, comm, data)
|
||||
|
||||
if !pA.provCalled {
|
||||
if !pA.ProvCalled {
|
||||
t.Error("provision should be called on pA")
|
||||
}
|
||||
|
||||
if !pB.provCalled {
|
||||
if !pB.ProvCalled {
|
||||
t.Error("provision should be called on pB")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvisionHook_cancel(t *testing.T) {
|
||||
var lock sync.Mutex
|
||||
order := make([]string, 0, 2)
|
||||
|
||||
p := &MockProvisioner{
|
||||
ProvFunc: func() error {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
order = append(order, "prov")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
hook := &ProvisionHook{
|
||||
Provisioners: []Provisioner{p},
|
||||
}
|
||||
|
||||
finished := make(chan struct{})
|
||||
go func() {
|
||||
hook.Run("foo", nil, nil, nil)
|
||||
close(finished)
|
||||
}()
|
||||
|
||||
// Cancel it while it is running
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
hook.Cancel()
|
||||
lock.Lock()
|
||||
order = append(order, "cancel")
|
||||
lock.Unlock()
|
||||
|
||||
// Wait
|
||||
<-finished
|
||||
|
||||
// Verify order
|
||||
if order[0] != "cancel" || order[1] != "prov" {
|
||||
t.Fatalf("bad: %#v", order)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(mitchellh): Test that they're run in the proper order
|
||||
|
|
|
@ -72,7 +72,7 @@ func TestBuilderRPC(t *testing.T) {
|
|||
|
||||
// Test Run
|
||||
cache := new(testCache)
|
||||
hook := &testHook{}
|
||||
hook := &packer.MockHook{}
|
||||
ui := &testUi{}
|
||||
artifact, err := bClient.Run(ui, hook, cache)
|
||||
assert.Nil(err, "should have no error")
|
||||
|
@ -83,7 +83,7 @@ func TestBuilderRPC(t *testing.T) {
|
|||
assert.True(cache.lockCalled, "lock should be called")
|
||||
|
||||
b.runHook.Run("foo", nil, nil, nil)
|
||||
assert.True(hook.runCalled, "run should be called")
|
||||
assert.True(hook.RunCalled, "run should be called")
|
||||
|
||||
b.runUi.Say("format")
|
||||
assert.True(ui.sayCalled, "say should be called")
|
||||
|
|
|
@ -2,6 +2,7 @@ package rpc
|
|||
|
||||
import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"net/rpc"
|
||||
)
|
||||
|
||||
|
@ -37,6 +38,13 @@ func (h *hook) Run(name string, ui packer.Ui, comm packer.Communicator, data int
|
|||
return h.client.Call("Hook.Run", args, new(interface{}))
|
||||
}
|
||||
|
||||
func (h *hook) Cancel() {
|
||||
err := h.client.Call("Hook.Cancel", new(interface{}), new(interface{}))
|
||||
if err != nil {
|
||||
log.Printf("Hook.Cancel error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HookServer) Run(args *HookRunArgs, reply *interface{}) error {
|
||||
client, err := rpc.Dial("tcp", args.RPCAddress)
|
||||
if err != nil {
|
||||
|
@ -50,3 +58,8 @@ func (h *HookServer) Run(args *HookRunArgs, reply *interface{}) error {
|
|||
*reply = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HookServer) Cancel(args *interface{}, reply *interface{}) error {
|
||||
h.hook.Cancel()
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -4,24 +4,17 @@ import (
|
|||
"cgl.tideland.biz/asserts"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"net/rpc"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type testHook struct {
|
||||
runCalled bool
|
||||
runUi packer.Ui
|
||||
}
|
||||
|
||||
func (h *testHook) Run(name string, ui packer.Ui, comm packer.Communicator, data interface{}) error {
|
||||
h.runCalled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestHookRPC(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
// Create the UI to test
|
||||
h := new(testHook)
|
||||
h := new(packer.MockHook)
|
||||
|
||||
// Serve
|
||||
server := rpc.NewServer()
|
||||
|
@ -37,7 +30,11 @@ func TestHookRPC(t *testing.T) {
|
|||
// Test Run
|
||||
ui := &testUi{}
|
||||
hClient.Run("foo", ui, nil, 42)
|
||||
assert.True(h.runCalled, "run should be called")
|
||||
assert.True(h.RunCalled, "run should be called")
|
||||
|
||||
// Test Cancel
|
||||
hClient.Cancel()
|
||||
assert.True(h.CancelCalled, "cancel should be called")
|
||||
}
|
||||
|
||||
func TestHook_Implements(t *testing.T) {
|
||||
|
@ -48,3 +45,56 @@ func TestHook_Implements(t *testing.T) {
|
|||
|
||||
assert.Implementor(h, &r, "should be a Hook")
|
||||
}
|
||||
|
||||
func TestHook_cancelWhileRun(t *testing.T) {
|
||||
var finishLock sync.Mutex
|
||||
finishOrder := make([]string, 0, 2)
|
||||
|
||||
h := &packer.MockHook{
|
||||
RunFunc: func() error {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
finishLock.Lock()
|
||||
finishOrder = append(finishOrder, "run")
|
||||
finishLock.Unlock()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Serve
|
||||
server := rpc.NewServer()
|
||||
RegisterHook(server, h)
|
||||
address := serveSingleConn(server)
|
||||
|
||||
// Create the client over RPC and run some methods to verify it works
|
||||
client, err := rpc.Dial("tcp", address)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
hClient := Hook(client)
|
||||
|
||||
// Start the run
|
||||
finished := make(chan struct{})
|
||||
go func() {
|
||||
hClient.Run("foo", nil, nil, nil)
|
||||
close(finished)
|
||||
}()
|
||||
|
||||
// Cancel it pretty quickly.
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
hClient.Cancel()
|
||||
|
||||
finishLock.Lock()
|
||||
finishOrder = append(finishOrder, "cancel")
|
||||
finishLock.Unlock()
|
||||
|
||||
// Verify things are good
|
||||
<-finished
|
||||
|
||||
// Check the results
|
||||
expected := []string{"cancel", "run"}
|
||||
if !reflect.DeepEqual(finishOrder, expected) {
|
||||
t.Fatalf("bad: %#v", finishOrder)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package rpc
|
|||
|
||||
import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"net/rpc"
|
||||
)
|
||||
|
||||
|
@ -47,6 +48,13 @@ func (p *provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
|
|||
return p.client.Call("Provisioner.Provision", args, new(interface{}))
|
||||
}
|
||||
|
||||
func (p *provisioner) Cancel() {
|
||||
err := p.client.Call("Provisioner.Cancel", new(interface{}), new(interface{}))
|
||||
if err != nil {
|
||||
log.Printf("Provisioner.Cancel err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ProvisionerServer) Prepare(args *ProvisionerPrepareArgs, reply *error) error {
|
||||
*reply = p.p.Prepare(args.Configs...)
|
||||
if *reply != nil {
|
||||
|
@ -71,3 +79,8 @@ func (p *ProvisionerServer) Provision(args *ProvisionerProvisionArgs, reply *int
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ProvisionerServer) Cancel(args *interface{}, reply *interface{}) error {
|
||||
p.p.Cancel()
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -7,32 +7,11 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
type testProvisioner struct {
|
||||
prepareCalled bool
|
||||
prepareConfigs []interface{}
|
||||
provCalled bool
|
||||
provComm packer.Communicator
|
||||
provUi packer.Ui
|
||||
}
|
||||
|
||||
func (p *testProvisioner) Prepare(configs ...interface{}) error {
|
||||
p.prepareCalled = true
|
||||
p.prepareConfigs = configs
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *testProvisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
|
||||
p.provCalled = true
|
||||
p.provComm = comm
|
||||
p.provUi = ui
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestProvisionerRPC(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
// Create the interface to test
|
||||
p := new(testProvisioner)
|
||||
p := new(packer.MockProvisioner)
|
||||
|
||||
// Start the server
|
||||
server := rpc.NewServer()
|
||||
|
@ -47,17 +26,23 @@ func TestProvisionerRPC(t *testing.T) {
|
|||
config := 42
|
||||
pClient := Provisioner(client)
|
||||
pClient.Prepare(config)
|
||||
assert.True(p.prepareCalled, "prepare should be called")
|
||||
assert.Equal(p.prepareConfigs, []interface{}{42}, "prepare should be called with right arg")
|
||||
assert.True(p.PrepCalled, "prepare should be called")
|
||||
assert.Equal(p.PrepConfigs, []interface{}{42}, "prepare should be called with right arg")
|
||||
|
||||
// Test Provision
|
||||
ui := &testUi{}
|
||||
comm := new(packer.MockCommunicator)
|
||||
comm := &packer.MockCommunicator{}
|
||||
pClient.Provision(ui, comm)
|
||||
assert.True(p.provCalled, "provision should be called")
|
||||
assert.True(p.ProvCalled, "provision should be called")
|
||||
|
||||
p.provUi.Say("foo")
|
||||
p.ProvUi.Say("foo")
|
||||
assert.True(ui.sayCalled, "say should be called")
|
||||
|
||||
// Test Cancel
|
||||
pClient.Cancel()
|
||||
if !p.CancelCalled {
|
||||
t.Fatal("cancel should be called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvisioner_Implements(t *testing.T) {
|
||||
|
|
|
@ -589,7 +589,7 @@ func TestTemplate_Build(t *testing.T) {
|
|||
"test-builder": builder,
|
||||
}
|
||||
|
||||
provisioner := &TestProvisioner{}
|
||||
provisioner := &MockProvisioner{}
|
||||
provisionerMap := map[string]Provisioner{
|
||||
"test-prov": provisioner,
|
||||
}
|
||||
|
@ -677,7 +677,7 @@ func TestTemplate_Build_ProvisionerOverride(t *testing.T) {
|
|||
"test-builder": builder,
|
||||
}
|
||||
|
||||
provisioner := &TestProvisioner{}
|
||||
provisioner := &MockProvisioner{}
|
||||
provisionerMap := map[string]Provisioner{
|
||||
"test-prov": provisioner,
|
||||
}
|
||||
|
|
|
@ -183,6 +183,12 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *Provisioner) Cancel() {
|
||||
// Just hard quit. It isn't a big deal if what we're doing keeps
|
||||
// running on the other side.
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, dst string, src string) error {
|
||||
if err := p.createDir(ui, comm, dst); err != nil {
|
||||
return err
|
||||
|
|
|
@ -84,3 +84,9 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
|
|||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *Provisioner) Cancel() {
|
||||
// Just hard quit. It isn't a big deal if what we're doing keeps
|
||||
// running on the other side.
|
||||
os.Exit(0)
|
||||
}
|
||||
|
|
|
@ -200,6 +200,12 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *Provisioner) Cancel() {
|
||||
// Just hard quit. It isn't a big deal if what we're doing keeps
|
||||
// running on the other side.
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func UploadLocalDirectory(localDir string, remoteDir string, comm packer.Communicator, ui packer.Ui) (err error) {
|
||||
visitPath := func(localPath string, f os.FileInfo, err error) (err2 error) {
|
||||
localRelPath := strings.Replace(localPath, localDir, "", 1)
|
||||
|
|
|
@ -281,6 +281,12 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *Provisioner) Cancel() {
|
||||
// Just hard quit. It isn't a big deal if what we're doing keeps
|
||||
// running on the other side.
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// retryable will retry the given function over and over until a
|
||||
// non-error is returned.
|
||||
func (p *Provisioner) retryable(f func() error) error {
|
||||
|
|
Loading…
Reference in New Issue