Merge pull request #739 from mitchellh/f-prov-pause-before

Provisioner "pause_before" meta-parameter
This commit is contained in:
Mitchell Hashimoto 2013-12-20 21:53:24 -08:00
commit 6588fe627d
6 changed files with 323 additions and 7 deletions

View File

@ -1,7 +1,9 @@
package packer package packer
import ( import (
"fmt"
"sync" "sync"
"time"
) )
// A provisioner is responsible for installing and configuring software // A provisioner is responsible for installing and configuring software
@ -65,3 +67,80 @@ func (h *ProvisionHook) Cancel() {
h.runningProvisioner.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)
}

View File

@ -5,11 +5,12 @@ package packer
type MockProvisioner struct { type MockProvisioner struct {
ProvFunc func() error ProvFunc func() error
PrepCalled bool PrepCalled bool
PrepConfigs []interface{} PrepConfigs []interface{}
ProvCalled bool ProvCalled bool
ProvUi Ui ProvCommunicator Communicator
CancelCalled bool ProvUi Ui
CancelCalled bool
} }
func (t *MockProvisioner) Prepare(configs ...interface{}) error { 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 { func (t *MockProvisioner) Provision(ui Ui, comm Communicator) error {
t.ProvCalled = true t.ProvCalled = true
t.ProvCommunicator = comm
t.ProvUi = ui t.ProvUi = ui
if t.ProvFunc == nil { if t.ProvFunc == nil {

View File

@ -80,3 +80,94 @@ func TestProvisionHook_cancel(t *testing.T) {
} }
// TODO(mitchellh): Test that they're run in the proper order // 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")
}
}

View File

@ -9,6 +9,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"sort" "sort"
"time"
) )
// The rawTemplate struct represents the structure of a template read // The rawTemplate struct represents the structure of a template read
@ -63,10 +64,13 @@ type RawPostProcessorConfig struct {
type RawProvisionerConfig struct { type RawProvisionerConfig struct {
TemplateOnlyExcept `mapstructure:",squash"` TemplateOnlyExcept `mapstructure:",squash"`
Type string Type string
Override map[string]interface{} Override map[string]interface{}
RawPauseBefore string `mapstructure:"pause_before"`
RawConfig interface{} RawConfig interface{}
pauseBefore time.Duration
} }
// RawVariable represents a variable configuration within a template. // 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 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} coreProv := coreBuildProvisioner{provisioner, configs}
provisioners = append(provisioners, coreProv) provisioners = append(provisioners, coreProv)
} }

View File

@ -6,6 +6,7 @@ import (
"reflect" "reflect"
"sort" "sort"
"testing" "testing"
"time"
) )
func testTemplateComponentFinder() *ComponentFinder { 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) { func TestParseTemplate_Variables(t *testing.T) {
data := ` 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) { func TestTemplateBuild_variables(t *testing.T) {
data := ` data := `
{ {

View File

@ -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 The value of this is in turn another JSON object. This JSON object simply
contains the provisioner configuration as normal. This configuration is merged contains the provisioner configuration as normal. This configuration is merged
into the default provisioner configuration. 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:
<pre class="prettyprint">
{
"type": "shell",
"script": "script.sh",
"pause_before": "10s"
}
</pre>
For the above provisioner, Packer will wait 10 seconds before uploading
and executing the shell script.