diff --git a/packer/provisioner.go b/packer/provisioner.go index 3592fb919..d28d1371a 100644 --- a/packer/provisioner.go +++ b/packer/provisioner.go @@ -1,7 +1,9 @@ package packer import ( + "fmt" "sync" + "time" ) // A provisioner is responsible for installing and configuring software @@ -65,3 +67,80 @@ func (h *ProvisionHook) Cancel() { h.runningProvisioner.Cancel() } } + +// PausedProvisioner is a Provisioner implementation that pauses before +// the provisioner is actually run. +type PausedProvisioner struct { + PauseBefore time.Duration + Provisioner Provisioner + + cancelCh chan struct{} + doneCh chan struct{} + lock sync.Mutex +} + +func (p *PausedProvisioner) Prepare(raws ...interface{}) error { + return p.Provisioner.Prepare(raws...) +} + +func (p *PausedProvisioner) Provision(ui Ui, comm Communicator) error { + p.lock.Lock() + cancelCh := make(chan struct{}) + p.cancelCh = cancelCh + + // Setup the done channel, which is trigger when we're done + doneCh := make(chan struct{}) + defer close(doneCh) + p.doneCh = doneCh + p.lock.Unlock() + + defer func() { + p.lock.Lock() + defer p.lock.Unlock() + if p.cancelCh == cancelCh { + p.cancelCh = nil + } + if p.doneCh == doneCh { + p.doneCh = nil + } + }() + + // Use a select to determine if we get cancelled during the wait + ui.Say(fmt.Sprintf("Pausing %s before the next provisioner...", p.PauseBefore)) + select { + case <-time.After(p.PauseBefore): + case <-cancelCh: + return nil + } + + provDoneCh := make(chan error, 1) + go p.provision(provDoneCh, ui, comm) + + select { + case err := <-provDoneCh: + return err + case <-cancelCh: + p.Provisioner.Cancel() + return <-provDoneCh + } +} + +func (p *PausedProvisioner) Cancel() { + var doneCh chan struct{} + + p.lock.Lock() + if p.cancelCh != nil { + close(p.cancelCh) + p.cancelCh = nil + } + if p.doneCh != nil { + doneCh = p.doneCh + } + p.lock.Unlock() + + <-doneCh +} + +func (p *PausedProvisioner) provision(result chan<- error, ui Ui, comm Communicator) { + result <- p.Provisioner.Provision(ui, comm) +} diff --git a/packer/provisioner_mock.go b/packer/provisioner_mock.go index b61f642af..62b304ccb 100644 --- a/packer/provisioner_mock.go +++ b/packer/provisioner_mock.go @@ -5,11 +5,12 @@ package packer type MockProvisioner struct { ProvFunc func() error - PrepCalled bool - PrepConfigs []interface{} - ProvCalled bool - ProvUi Ui - CancelCalled bool + PrepCalled bool + PrepConfigs []interface{} + ProvCalled bool + ProvCommunicator Communicator + ProvUi Ui + CancelCalled bool } func (t *MockProvisioner) Prepare(configs ...interface{}) error { @@ -20,6 +21,7 @@ func (t *MockProvisioner) Prepare(configs ...interface{}) error { func (t *MockProvisioner) Provision(ui Ui, comm Communicator) error { t.ProvCalled = true + t.ProvCommunicator = comm t.ProvUi = ui if t.ProvFunc == nil { diff --git a/packer/provisioner_test.go b/packer/provisioner_test.go index a3d97d511..5eeebb4a3 100644 --- a/packer/provisioner_test.go +++ b/packer/provisioner_test.go @@ -80,3 +80,94 @@ func TestProvisionHook_cancel(t *testing.T) { } // TODO(mitchellh): Test that they're run in the proper order + +func TestPausedProvisioner_impl(t *testing.T) { + var _ Provisioner = new(PausedProvisioner) +} + +func TestPausedProvisionerPrepare(t *testing.T) { + mock := new(MockProvisioner) + prov := &PausedProvisioner{ + Provisioner: mock, + } + + prov.Prepare(42) + if !mock.PrepCalled { + t.Fatal("prepare should be called") + } + if mock.PrepConfigs[0] != 42 { + t.Fatal("should have proper configs") + } +} + +func TestPausedProvisionerProvision(t *testing.T) { + mock := new(MockProvisioner) + prov := &PausedProvisioner{ + Provisioner: mock, + } + + ui := testUi() + comm := new(MockCommunicator) + prov.Provision(ui, comm) + if !mock.ProvCalled { + t.Fatal("prov should be called") + } + if mock.ProvUi != ui { + t.Fatal("should have proper ui") + } + if mock.ProvCommunicator != comm { + t.Fatal("should have proper comm") + } +} + +func TestPausedProvisionerProvision_waits(t *testing.T) { + mock := new(MockProvisioner) + prov := &PausedProvisioner{ + PauseBefore: 50 * time.Millisecond, + Provisioner: mock, + } + + dataCh := make(chan struct{}) + mock.ProvFunc = func() error { + close(dataCh) + return nil + } + + go prov.Provision(testUi(), new(MockCommunicator)) + + select { + case <-time.After(10 * time.Millisecond): + case <-dataCh: + t.Fatal("should not be called") + } + + select { + case <-time.After(100 * time.Millisecond): + t.Fatal("never called") + case <-dataCh: + } +} + +func TestPausedProvisionerCancel(t *testing.T) { + mock := new(MockProvisioner) + prov := &PausedProvisioner{ + Provisioner: mock, + } + + provCh := make(chan struct{}) + mock.ProvFunc = func() error { + close(provCh) + time.Sleep(10 * time.Millisecond) + return nil + } + + // Start provisioning and wait for it to start + go prov.Provision(testUi(), new(MockCommunicator)) + <-provCh + + // Cancel it + prov.Cancel() + if !mock.CancelCalled { + t.Fatal("cancel should be called") + } +} diff --git a/packer/template.go b/packer/template.go index 6c601d05b..346e5a5a0 100644 --- a/packer/template.go +++ b/packer/template.go @@ -9,6 +9,7 @@ import ( "io/ioutil" "os" "sort" + "time" ) // The rawTemplate struct represents the structure of a template read @@ -63,10 +64,13 @@ type RawPostProcessorConfig struct { type RawProvisionerConfig struct { TemplateOnlyExcept `mapstructure:",squash"` - Type string - Override map[string]interface{} + Type string + Override map[string]interface{} + RawPauseBefore string `mapstructure:"pause_before"` RawConfig interface{} + + pauseBefore time.Duration } // RawVariable represents a variable configuration within a template. @@ -289,6 +293,19 @@ func ParseTemplate(data []byte) (t *Template, err error) { } } + // Setup the pause settings + if raw.RawPauseBefore != "" { + duration, err := time.ParseDuration(raw.RawPauseBefore) + if err != nil { + errors = append( + errors, fmt.Errorf( + "provisioner %d: pause_before invalid: %s", + i+1, err)) + } + + raw.pauseBefore = duration + } + raw.RawConfig = v } @@ -498,6 +515,13 @@ func (t *Template) Build(name string, components *ComponentFinder) (b Build, err } } + if rawProvisioner.pauseBefore > 0 { + provisioner = &PausedProvisioner{ + PauseBefore: rawProvisioner.pauseBefore, + Provisioner: provisioner, + } + } + coreProv := coreBuildProvisioner{provisioner, configs} provisioners = append(provisioners, coreProv) } diff --git a/packer/template_test.go b/packer/template_test.go index 053a4705d..800c982d0 100644 --- a/packer/template_test.go +++ b/packer/template_test.go @@ -6,6 +6,7 @@ import ( "reflect" "sort" "testing" + "time" ) func testTemplateComponentFinder() *ComponentFinder { @@ -445,6 +446,38 @@ func TestParseTemplate_Provisioners(t *testing.T) { } } +func TestParseTemplate_ProvisionerPauseBefore(t *testing.T) { + data := ` + { + "builders": [{"type": "foo"}], + + "provisioners": [ + { + "type": "shell", + "pause_before": "10s" + } + ] + } + ` + + result, err := ParseTemplate([]byte(data)) + if err != nil { + t.Fatal("err: %s", err) + } + if result == nil { + t.Fatal("should have result") + } + if len(result.Provisioners) != 1 { + t.Fatalf("bad: %#v", result.Provisioners) + } + if result.Provisioners[0].Type != "shell" { + t.Fatalf("bad: %#v", result.Provisioners[0].Type) + } + if result.Provisioners[0].pauseBefore != 10*time.Second { + t.Fatalf("bad: %s", result.Provisioners[0].pauseBefore) + } +} + func TestParseTemplate_Variables(t *testing.T) { data := ` { @@ -1278,6 +1311,70 @@ func TestTemplate_Build_ProvisionerOverrideBad(t *testing.T) { } } +func TestTemplateBuild_ProvisionerPauseBefore(t *testing.T) { + data := ` + { + "builders": [ + { + "name": "test1", + "type": "test-builder" + } + ], + + "provisioners": [ + { + "type": "test-prov", + "pause_before": "5s" + } + ] + } + ` + + template, err := ParseTemplate([]byte(data)) + if err != nil { + t.Fatalf("err: %s", err) + } + + builder := new(MockBuilder) + builderMap := map[string]Builder{ + "test-builder": builder, + } + + provisioner := &MockProvisioner{} + provisionerMap := map[string]Provisioner{ + "test-prov": provisioner, + } + + builderFactory := func(n string) (Builder, error) { return builderMap[n], nil } + provFactory := func(n string) (Provisioner, error) { return provisionerMap[n], nil } + components := &ComponentFinder{ + Builder: builderFactory, + Provisioner: provFactory, + } + + // Get the build, verifying we can get it without issue, but also + // that the proper builder was looked up and used for the build. + build, err := template.Build("test1", components) + if err != nil { + t.Fatalf("err: %s", err) + } + + coreBuild, ok := build.(*coreBuild) + if !ok { + t.Fatal("should be okay") + } + if len(coreBuild.provisioners) != 1 { + t.Fatalf("bad: %#v", coreBuild.provisioners) + } + if pp, ok := coreBuild.provisioners[0].provisioner.(*PausedProvisioner); !ok { + t.Fatalf("should be paused provisioner") + } else { + if pp.PauseBefore != 5*time.Second { + t.Fatalf("bad: %#v", pp.PauseBefore) + } + } +} + func TestTemplateBuild_variables(t *testing.T) { data := ` { diff --git a/website/source/docs/templates/provisioners.html.markdown b/website/source/docs/templates/provisioners.html.markdown index eb49788e1..327ae46e7 100644 --- a/website/source/docs/templates/provisioners.html.markdown +++ b/website/source/docs/templates/provisioners.html.markdown @@ -112,3 +112,26 @@ JSON object where the key is the name of a [builder definition](/docs/templates/ The value of this is in turn another JSON object. This JSON object simply contains the provisioner configuration as normal. This configuration is merged into the default provisioner configuration. + +## Pausing Before Running + +With certain provisioners it is sometimes desirable to pause for some period +of time before running it. Specifically, in cases where a provisioner reboots +the machine, you may want to wait for some period of time before starting +the next provisioner. + +Every provisioner definition in a Packer template can take a special +configuration `pause_before` that is the amount of time to pause before +running that provisioner. By default, there is no pause. An example +is shown below: + +
+{
+  "type": "shell",
+  "script": "script.sh",
+  "pause_before": "10s"
+}
+
+ +For the above provisioner, Packer will wait 10 seconds before uploading +and executing the shell script.