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
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)
}

View File

@ -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 {

View File

@ -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")
}
}

View File

@ -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)
}

View File

@ -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 := `
{

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
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:
<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.