Add RetriedProvisioner to allow retry provisioners (#9061)

This commit is contained in:
Sylvia Moss 2020-04-16 11:58:54 +02:00 committed by GitHub
parent d580ea7950
commit 553b1fb9f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 532 additions and 7 deletions

View File

@ -51,6 +51,7 @@ commands:
jobs: jobs:
test-linux: test-linux:
executor: golang executor: golang
resource_class: large
working_directory: /go/src/github.com/hashicorp/packer working_directory: /go/src/github.com/hashicorp/packer
steps: steps:
- checkout - checkout

View File

@ -207,6 +207,23 @@ var (
}, },
} }
emptyMockBuilder = &MockBuilder{
Config: MockConfig{
NestedMockConfig: NestedMockConfig{
Tags: []MockTag{},
},
Nested: NestedMockConfig{},
NestedSlice: []NestedMockConfig{},
},
}
emptyMockProvisioner = &MockProvisioner{
Config: MockConfig{
NestedMockConfig: NestedMockConfig{Tags: []MockTag{}},
NestedSlice: []NestedMockConfig{},
},
}
dynamicTagList = []MockTag{ dynamicTagList = []MockTag{
{ {
Key: "first_tag_key", Key: "first_tag_key",

View File

@ -0,0 +1,15 @@
// starts resources to provision them.
build {
sources = [
"source.virtualbox-iso.ubuntu-1204"
]
provisioner "shell" {
pause_before = "10s"
max_retries = 5
}
}
source "virtualbox-iso" "ubuntu-1204" {
}

View File

@ -0,0 +1,14 @@
// starts resources to provision them.
build {
sources = [
"source.virtualbox-iso.ubuntu-1204"
]
provisioner "shell" {
timeout = "10s"
}
}
source "virtualbox-iso" "ubuntu-1204" {
}

View File

@ -2,6 +2,7 @@ package hcl2template
import ( import (
"fmt" "fmt"
"time"
"github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/hcl/v2/gohcl"
@ -10,8 +11,11 @@ import (
// ProvisionerBlock references a detected but unparsed provisioner // ProvisionerBlock references a detected but unparsed provisioner
type ProvisionerBlock struct { type ProvisionerBlock struct {
PType string PType string
PName string PName string
PauseBefore time.Duration
MaxRetries int
Timeout time.Duration
HCL2Ref HCL2Ref
} }
@ -21,17 +25,44 @@ func (p *ProvisionerBlock) String() string {
func (p *Parser) decodeProvisioner(block *hcl.Block) (*ProvisionerBlock, hcl.Diagnostics) { func (p *Parser) decodeProvisioner(block *hcl.Block) (*ProvisionerBlock, hcl.Diagnostics) {
var b struct { var b struct {
Name string `hcl:"name,optional"` Name string `hcl:"name,optional"`
Rest hcl.Body `hcl:",remain"` PauseBefore string `hcl:"pause_before,optional"`
MaxRetries int `hcl:"max_retries,optional"`
Timeout string `hcl:"timeout,optional"`
Rest hcl.Body `hcl:",remain"`
} }
diags := gohcl.DecodeBody(block.Body, nil, &b) diags := gohcl.DecodeBody(block.Body, nil, &b)
if diags.HasErrors() { if diags.HasErrors() {
return nil, diags return nil, diags
} }
provisioner := &ProvisionerBlock{ provisioner := &ProvisionerBlock{
PType: block.Labels[0], PType: block.Labels[0],
PName: b.Name, PName: b.Name,
HCL2Ref: newHCL2Ref(block, b.Rest), MaxRetries: b.MaxRetries,
HCL2Ref: newHCL2Ref(block, b.Rest),
}
if b.PauseBefore != "" {
pauseBefore, err := time.ParseDuration(b.PauseBefore)
if err != nil {
return nil, append(diags, &hcl.Diagnostic{
Summary: "Failed to parse pause_before duration",
Detail: err.Error(),
})
}
provisioner.PauseBefore = pauseBefore
}
if b.Timeout != "" {
timeout, err := time.ParseDuration(b.Timeout)
if err != nil {
return nil, append(diags, &hcl.Diagnostic{
Summary: "Failed to parse timeout duration",
Detail: err.Error(),
})
}
provisioner.Timeout = timeout
} }
if !p.ProvisionersSchemas.Has(provisioner.PType) { if !p.ProvisionersSchemas.Has(provisioner.PType) {

View File

@ -195,6 +195,26 @@ func (p *Parser) getCoreBuildProvisioners(source *SourceBlock, blocks []*Provisi
if moreDiags.HasErrors() { if moreDiags.HasErrors() {
continue continue
} }
// If we're pausing, we wrap the provisioner in a special pauser.
if pb.PauseBefore != 0 {
provisioner = &packer.PausedProvisioner{
PauseBefore: pb.PauseBefore,
Provisioner: provisioner,
}
} else if pb.Timeout != 0 {
provisioner = &packer.TimeoutProvisioner{
Timeout: pb.Timeout,
Provisioner: provisioner,
}
}
if pb.MaxRetries != 0 {
provisioner = &packer.RetriedProvisioner{
MaxRetries: pb.MaxRetries,
Provisioner: provisioner,
}
}
res = append(res, packer.CoreBuildProvisioner{ res = append(res, packer.CoreBuildProvisioner{
PType: pb.PType, PType: pb.PType,
PName: pb.PName, PName: pb.PName,

View File

@ -1,7 +1,9 @@
package hcl2template package hcl2template
import ( import (
"path/filepath"
"testing" "testing"
"time"
"github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/packer"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
@ -173,6 +175,90 @@ func TestParser_complete(t *testing.T) {
parseWantDiags: true, parseWantDiags: true,
parseWantDiagHasErrors: true, parseWantDiagHasErrors: true,
}, },
{"provisioner with wrappers pause_before and max_retriers",
defaultParser,
parseTestArgs{"testdata/build/provisioner_paused_before_retry.pkr.hcl", nil, nil},
&PackerConfig{
Basedir: filepath.Join("testdata", "build"),
Sources: map[SourceRef]*SourceBlock{
refVBIsoUbuntu1204: {Type: "virtualbox-iso", Name: "ubuntu-1204"},
},
Builds: Builds{
&BuildBlock{
Sources: []SourceRef{refVBIsoUbuntu1204},
ProvisionerBlocks: []*ProvisionerBlock{
{
PType: "shell",
PauseBefore: time.Second * 10,
MaxRetries: 5,
},
},
},
},
},
false, false,
[]packer.Build{
&packer.CoreBuild{
Type: "virtualbox-iso",
Prepared: true,
Builder: emptyMockBuilder,
Provisioners: []packer.CoreBuildProvisioner{
{
PType: "shell",
Provisioner: &packer.RetriedProvisioner{
MaxRetries: 5,
Provisioner: &packer.PausedProvisioner{
PauseBefore: time.Second * 10,
Provisioner: emptyMockProvisioner,
},
},
},
},
PostProcessors: [][]packer.CoreBuildPostProcessor{},
},
},
false,
},
{"provisioner with wrappers timeout",
defaultParser,
parseTestArgs{"testdata/build/provisioner_timeout.pkr.hcl", nil, nil},
&PackerConfig{
Basedir: filepath.Join("testdata", "build"),
Sources: map[SourceRef]*SourceBlock{
refVBIsoUbuntu1204: {Type: "virtualbox-iso", Name: "ubuntu-1204"},
},
Builds: Builds{
&BuildBlock{
Sources: []SourceRef{refVBIsoUbuntu1204},
ProvisionerBlocks: []*ProvisionerBlock{
{
PType: "shell",
Timeout: time.Second * 10,
},
},
},
},
},
false, false,
[]packer.Build{
&packer.CoreBuild{
Type: "virtualbox-iso",
Prepared: true,
Builder: emptyMockBuilder,
Provisioners: []packer.CoreBuildProvisioner{
{
PType: "shell",
Provisioner: &packer.TimeoutProvisioner{
Timeout: time.Second * 10,
Provisioner: emptyMockProvisioner,
},
},
},
PostProcessors: [][]packer.CoreBuildPostProcessor{},
},
},
false,
},
} }
testParse(t, tests) testParse(t, tests)
} }

View File

@ -155,6 +155,7 @@ type FlatMockProvisioner struct {
PrepCalled *bool `cty:"prep_called"` PrepCalled *bool `cty:"prep_called"`
PrepConfigs []interface{} `cty:"prep_configs"` PrepConfigs []interface{} `cty:"prep_configs"`
ProvCalled *bool `cty:"prov_called"` ProvCalled *bool `cty:"prov_called"`
ProvRetried *bool `cty:"prov_retried"`
ProvCommunicator Communicator `cty:"prov_communicator"` ProvCommunicator Communicator `cty:"prov_communicator"`
ProvUi Ui `cty:"prov_ui"` ProvUi Ui `cty:"prov_ui"`
} }
@ -174,6 +175,7 @@ func (*FlatMockProvisioner) HCL2Spec() map[string]hcldec.Spec {
"prep_called": &hcldec.AttrSpec{Name: "prep_called", Type: cty.Bool, Required: false}, "prep_called": &hcldec.AttrSpec{Name: "prep_called", Type: cty.Bool, Required: false},
"prep_configs": &hcldec.AttrSpec{Name: "prep_configs", Type: cty.Bool, Required: false}, /* TODO(azr): could not find type */ "prep_configs": &hcldec.AttrSpec{Name: "prep_configs", Type: cty.Bool, Required: false}, /* TODO(azr): could not find type */
"prov_called": &hcldec.AttrSpec{Name: "prov_called", Type: cty.Bool, Required: false}, "prov_called": &hcldec.AttrSpec{Name: "prov_called", Type: cty.Bool, Required: false},
"prov_retried": &hcldec.AttrSpec{Name: "prov_retried", Type: cty.Bool, Required: false},
"prov_communicator": &hcldec.AttrSpec{Name: "prov_communicator", Type: cty.Bool, Required: false}, /* TODO(azr): could not find type */ "prov_communicator": &hcldec.AttrSpec{Name: "prov_communicator", Type: cty.Bool, Required: false}, /* TODO(azr): could not find type */
"prov_ui": &hcldec.AttrSpec{Name: "prov_ui", Type: cty.Bool, Required: false}, /* TODO(azr): could not find type */ "prov_ui": &hcldec.AttrSpec{Name: "prov_ui", Type: cty.Bool, Required: false}, /* TODO(azr): could not find type */
} }

View File

@ -170,6 +170,12 @@ func (c *Core) generateCoreBuildProvisioner(rawP *template.Provisioner, rawName
Provisioner: provisioner, Provisioner: provisioner,
} }
} }
if rawP.MaxRetries != 0 {
provisioner = &RetriedProvisioner{
MaxRetries: rawP.MaxRetries,
Provisioner: provisioner,
}
}
cbp = CoreBuildProvisioner{ cbp = CoreBuildProvisioner{
PType: rawP.Type, PType: rawP.Type,
Provisioner: provisioner, Provisioner: provisioner,

View File

@ -2,6 +2,7 @@ package packer
import ( import (
"context" "context"
"errors"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
@ -786,3 +787,42 @@ func testCoreTemplate(t *testing.T, c *CoreConfig, p string) {
c.Template = tpl c.Template = tpl
} }
func TestCoreBuild_provRetry(t *testing.T) {
config := TestCoreConfig(t)
testCoreTemplate(t, config, fixtureDir("build-prov-retry.json"))
b := TestBuilder(t, config, "test")
p := TestProvisioner(t, config, "test")
core := TestCore(t, config)
b.ArtifactId = "hello"
build, err := core.Build("test")
if err != nil {
t.Fatalf("err: %s", err)
}
if _, err := build.Prepare(); err != nil {
t.Fatalf("err: %s", err)
}
ui := testUi()
p.ProvFunc = func(ctx context.Context) error {
return errors.New("failed")
}
artifact, err := build.Run(context.Background(), ui)
if err != nil {
t.Fatalf("err: %s", err)
}
if len(artifact) != 1 {
t.Fatalf("bad: %#v", artifact)
}
if artifact[0].Id() != b.ArtifactId {
t.Fatalf("bad: %s", artifact[0].Id())
}
if !p.ProvRetried {
t.Fatal("provisioner should retry")
}
}

View File

@ -162,6 +162,48 @@ func (p *PausedProvisioner) Provision(ctx context.Context, ui Ui, comm Communica
return p.Provisioner.Provision(ctx, ui, comm, generatedData) return p.Provisioner.Provision(ctx, ui, comm, generatedData)
} }
// RetriedProvisioner is a Provisioner implementation that retries
// the provisioner whenever there's an error.
type RetriedProvisioner struct {
MaxRetries int
Provisioner Provisioner
}
func (r *RetriedProvisioner) ConfigSpec() hcldec.ObjectSpec { return r.ConfigSpec() }
func (r *RetriedProvisioner) FlatConfig() interface{} { return r.FlatConfig() }
func (r *RetriedProvisioner) Prepare(raws ...interface{}) error {
return r.Provisioner.Prepare(raws...)
}
func (r *RetriedProvisioner) Provision(ctx context.Context, ui Ui, comm Communicator, generatedData map[string]interface{}) error {
if ctx.Err() != nil { // context was cancelled
return ctx.Err()
}
err := r.Provisioner.Provision(ctx, ui, comm, generatedData)
if err == nil {
return nil
}
leftTries := r.MaxRetries
for ; leftTries > 0; leftTries-- {
if ctx.Err() != nil { // context was cancelled
return ctx.Err()
}
ui.Say(fmt.Sprintf("Provisioner failed with %q, retrying with %d trie(s) left", err, leftTries))
err := r.Provisioner.Provision(ctx, ui, comm, generatedData)
if err == nil {
return nil
}
}
ui.Say("retry limit reached.")
return err
}
// DebuggedProvisioner is a Provisioner implementation that waits until a key // DebuggedProvisioner is a Provisioner implementation that waits until a key
// press before the provisioner is actually run. // press before the provisioner is actually run.
type DebuggedProvisioner struct { type DebuggedProvisioner struct {

View File

@ -14,6 +14,7 @@ type MockProvisioner struct {
PrepCalled bool PrepCalled bool
PrepConfigs []interface{} PrepConfigs []interface{}
ProvCalled bool ProvCalled bool
ProvRetried bool
ProvCommunicator Communicator ProvCommunicator Communicator
ProvUi Ui ProvUi Ui
} }
@ -29,6 +30,11 @@ func (t *MockProvisioner) Prepare(configs ...interface{}) error {
} }
func (t *MockProvisioner) Provision(ctx context.Context, ui Ui, comm Communicator, generatedData map[string]interface{}) error { func (t *MockProvisioner) Provision(ctx context.Context, ui Ui, comm Communicator, generatedData map[string]interface{}) error {
if t.ProvCalled {
t.ProvRetried = true
return nil
}
t.ProvCalled = true t.ProvCalled = true
t.ProvCommunicator = comm t.ProvCommunicator = comm
t.ProvUi = ui t.ProvUi = ui

View File

@ -2,6 +2,7 @@ package packer
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"testing" "testing"
"time" "time"
@ -229,3 +230,114 @@ func TestDebuggedProvisionerCancel(t *testing.T) {
t.Fatal("should have error") t.Fatal("should have error")
} }
} }
func TestRetriedProvisioner_impl(t *testing.T) {
var _ Provisioner = new(RetriedProvisioner)
}
func TestRetriedProvisionerPrepare(t *testing.T) {
mock := new(MockProvisioner)
prov := &RetriedProvisioner{
Provisioner: mock,
}
err := prov.Prepare(42)
if err != nil {
t.Fatal("should not have errored")
}
if !mock.PrepCalled {
t.Fatal("prepare should be called")
}
if mock.PrepConfigs[0] != 42 {
t.Fatal("should have proper configs")
}
}
func TestRetriedProvisionerProvision(t *testing.T) {
mock := &MockProvisioner{
ProvFunc: func(ctx context.Context) error {
return errors.New("failed")
},
}
prov := &RetriedProvisioner{
MaxRetries: 2,
Provisioner: mock,
}
ui := testUi()
comm := new(MockCommunicator)
err := prov.Provision(context.Background(), ui, comm, make(map[string]interface{}))
if err != nil {
t.Fatal("should not have errored")
}
if !mock.ProvCalled {
t.Fatal("prov should be called")
}
if !mock.ProvRetried {
t.Fatal("prov should be retried")
}
if mock.ProvUi != ui {
t.Fatal("should have proper ui")
}
if mock.ProvCommunicator != comm {
t.Fatal("should have proper comm")
}
}
func TestRetriedProvisionerCancelledProvision(t *testing.T) {
// Don't retry if context is cancelled
ctx, topCtxCancel := context.WithCancel(context.Background())
mock := &MockProvisioner{
ProvFunc: func(ctx context.Context) error {
topCtxCancel()
<-ctx.Done()
return ctx.Err()
},
}
prov := &RetriedProvisioner{
MaxRetries: 2,
Provisioner: mock,
}
ui := testUi()
comm := new(MockCommunicator)
err := prov.Provision(ctx, ui, comm, make(map[string]interface{}))
if err == nil {
t.Fatal("should have errored")
}
if !mock.ProvCalled {
t.Fatal("prov should be called")
}
if mock.ProvRetried {
t.Fatal("prov should NOT be retried")
}
if mock.ProvUi != ui {
t.Fatal("should have proper ui")
}
if mock.ProvCommunicator != comm {
t.Fatal("should have proper comm")
}
}
func TestRetriedProvisionerCancel(t *testing.T) {
topCtx, cancelTopCtx := context.WithCancel(context.Background())
mock := new(MockProvisioner)
prov := &RetriedProvisioner{
Provisioner: mock,
}
mock.ProvFunc = func(ctx context.Context) error {
cancelTopCtx()
<-ctx.Done()
return ctx.Err()
}
err := prov.Provision(topCtx, testUi(), new(MockCommunicator), make(map[string]interface{}))
if err == nil {
t.Fatal("should have err")
}
}

View File

@ -0,0 +1,10 @@
{
"builders": [{
"type": "test"
}],
"provisioners": [{
"type": "test",
"max_retries": 1
}]
}

View File

@ -0,0 +1,64 @@
package shell_test
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/hashicorp/packer/helper/tests/acc"
"github.com/hashicorp/packer/provisioner/shell"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/command"
)
func TestShellLocalProvisionerWithRetryOption(t *testing.T) {
acc.TestProvisionersPreCheck("shell-local", t)
acc.TestProvisionersAgainstBuilders(new(ShellLocalProvisionerAccTest), t)
}
type ShellLocalProvisionerAccTest struct{}
func (s *ShellLocalProvisionerAccTest) GetName() string {
return "file"
}
func (s *ShellLocalProvisionerAccTest) GetConfig() (string, error) {
filePath := filepath.Join("./test-fixtures", "shell-local-provisioner.txt")
config, err := os.Open(filePath)
if err != nil {
return "", fmt.Errorf("Expected to find %s", filePath)
}
defer config.Close()
file, err := ioutil.ReadAll(config)
return string(file), err
}
func (s *ShellLocalProvisionerAccTest) GetProvisionerStore() packer.MapOfProvisioner {
return packer.MapOfProvisioner{
"shell-local": func() (packer.Provisioner, error) { return &shell.Provisioner{}, nil },
}
}
func (s *ShellLocalProvisionerAccTest) IsCompatible(builder string, vmOS string) bool {
return vmOS == "linux"
}
func (s *ShellLocalProvisionerAccTest) RunTest(c *command.BuildCommand, args []string) error {
if code := c.Run(args); code != 0 {
ui := c.Meta.Ui.(*packer.BasicUi)
out := ui.Writer.(*bytes.Buffer)
err := ui.ErrorWriter.(*bytes.Buffer)
return fmt.Errorf(
"Bad exit code.\n\nStdout:\n\n%s\n\nStderr:\n\n%s",
out.String(),
err.String())
}
return nil
}

View File

@ -0,0 +1,6 @@
#!/bin/bash
if [[ ! -f file.txt ]] ; then
echo 'hello' > file.txt
exit 1
fi

View File

@ -0,0 +1,5 @@
{
"type": "shell-local",
"script": "test-fixtures/script.sh",
"max_retries" : 5
}

View File

@ -76,6 +76,7 @@ func (r *rawTemplate) decodeProvisioner(raw interface{}) (Provisioner, error) {
delete(p.Config, "only") delete(p.Config, "only")
delete(p.Config, "override") delete(p.Config, "override")
delete(p.Config, "pause_before") delete(p.Config, "pause_before")
delete(p.Config, "max_retries")
delete(p.Config, "type") delete(p.Config, "type")
delete(p.Config, "timeout") delete(p.Config, "timeout")

View File

@ -106,6 +106,19 @@ func TestParse(t *testing.T) {
false, false,
}, },
{
"parse-provisioner-retry.json",
&Template{
Provisioners: []*Provisioner{
{
Type: "something",
MaxRetries: 5,
},
},
},
false,
},
{ {
"parse-provisioner-timeout.json", "parse-provisioner-timeout.json",
&Template{ &Template{

View File

@ -141,6 +141,7 @@ type Provisioner struct {
Config map[string]interface{} `json:"config,omitempty"` Config map[string]interface{} `json:"config,omitempty"`
Override map[string]interface{} `json:"override,omitempty"` Override map[string]interface{} `json:"override,omitempty"`
PauseBefore time.Duration `mapstructure:"pause_before" json:"pause_before,omitempty"` PauseBefore time.Duration `mapstructure:"pause_before" json:"pause_before,omitempty"`
MaxRetries int `mapstructure:"max_retries" json:"max_retries,omitempty"`
Timeout time.Duration `mapstructure:"timeout" json:"timeout,omitempty"` Timeout time.Duration `mapstructure:"timeout" json:"timeout,omitempty"`
} }

View File

@ -15,6 +15,7 @@ type FlatProvisioner struct {
Config map[string]interface{} `json:"config,omitempty" cty:"config"` Config map[string]interface{} `json:"config,omitempty" cty:"config"`
Override map[string]interface{} `json:"override,omitempty" cty:"override"` Override map[string]interface{} `json:"override,omitempty" cty:"override"`
PauseBefore *string `mapstructure:"pause_before" json:"pause_before,omitempty" cty:"pause_before"` PauseBefore *string `mapstructure:"pause_before" json:"pause_before,omitempty" cty:"pause_before"`
MaxRetries *int `mapstructure:"max_retries" json:"max_retries,omitempty" cty:"max_retries"`
Timeout *string `mapstructure:"timeout" json:"timeout,omitempty" cty:"timeout"` Timeout *string `mapstructure:"timeout" json:"timeout,omitempty" cty:"timeout"`
} }
@ -36,6 +37,7 @@ func (*FlatProvisioner) HCL2Spec() map[string]hcldec.Spec {
"config": &hcldec.AttrSpec{Name: "config", Type: cty.Map(cty.String), Required: false}, "config": &hcldec.AttrSpec{Name: "config", Type: cty.Map(cty.String), Required: false},
"override": &hcldec.AttrSpec{Name: "override", Type: cty.Map(cty.String), Required: false}, "override": &hcldec.AttrSpec{Name: "override", Type: cty.Map(cty.String), Required: false},
"pause_before": &hcldec.AttrSpec{Name: "pause_before", Type: cty.String, Required: false}, "pause_before": &hcldec.AttrSpec{Name: "pause_before", Type: cty.String, Required: false},
"max_retries": &hcldec.AttrSpec{Name: "max_retries", Type: cty.Number, Required: false},
"timeout": &hcldec.AttrSpec{Name: "timeout", Type: cty.String, Required: false}, "timeout": &hcldec.AttrSpec{Name: "timeout", Type: cty.String, Required: false},
} }
return s return s

View File

@ -0,0 +1,8 @@
{
"provisioners": [
{
"type": "something",
"max_retries": 5
}
]
}

View File

@ -177,6 +177,27 @@ that provisioner. By default, there is no pause. An example is shown below:
For the above provisioner, Packer will wait 10 seconds before uploading and For the above provisioner, Packer will wait 10 seconds before uploading and
executing the shell script. executing the shell script.
## Retry on error
With certain provisioners it is sometimes desirable to retry when it fails.
Specifically, in cases where the provisioner depends on external processes that are not done yet.
Every provisioner definition in a Packer template can take a special
configuration `max_retries` that is the maximum number of times a provisioner will retry on error.
By default, there `max_retries` is zero and there is no retry on error. An example is shown below:
```json
{
"type": "shell",
"script": "script.sh",
"max_retries": 5
}
```
For the above provisioner, Packer will retry maximum five times until stops failing.
If after five retries the provisioner still fails, then the complete build will fail.
## Timeout ## Timeout
Sometimes a command can take much more time than expected Sometimes a command can take much more time than expected

View File

@ -2,6 +2,8 @@ Parameters common to all provisioners:
- `pause_before` (duration) - Sleep for duration before execution. - `pause_before` (duration) - Sleep for duration before execution.
- `max_retries` (int) - Max times the provisioner will retry in case of failure. Defaults to zero (0). Zero means an error will not be retried.
- `only` (array of string) - Only run the provisioner for listed builder(s) - `only` (array of string) - Only run the provisioner for listed builder(s)
by name. by name.