From 553b1fb9f85b0dfd5a59812b4217661f8cc896bd Mon Sep 17 00:00:00 2001 From: Sylvia Moss Date: Thu, 16 Apr 2020 11:58:54 +0200 Subject: [PATCH] Add RetriedProvisioner to allow retry provisioners (#9061) --- .circleci/config.yml | 1 + hcl2template/common_test.go | 17 +++ .../provisioner_paused_before_retry.pkr.hcl | 15 +++ .../build/provisioner_timeout.pkr.hcl | 14 +++ hcl2template/types.build.provisioners.go | 45 +++++-- hcl2template/types.packer_config.go | 20 ++++ hcl2template/types.packer_config_test.go | 86 ++++++++++++++ packer/builder_mock.hcl2spec.go | 2 + packer/core.go | 6 + packer/core_test.go | 40 +++++++ packer/provisioner.go | 42 +++++++ packer/provisioner_mock.go | 6 + packer/provisioner_test.go | 112 ++++++++++++++++++ packer/test-fixtures/build-prov-retry.json | 10 ++ .../shell-local/provisioner_acc_test.go | 64 ++++++++++ .../shell-local/test-fixtures/script.sh | 6 + .../test-fixtures/shell-local-provisioner.txt | 5 + template/parse.go | 1 + template/parse_test.go | 13 ++ template/template.go | 1 + template/template.hcl2spec.go | 2 + .../parse-provisioner-retry.json | 8 ++ website/pages/docs/templates/provisioners.mdx | 21 ++++ .../partials/provisioners/common-config.mdx | 2 + 24 files changed, 532 insertions(+), 7 deletions(-) create mode 100644 hcl2template/testdata/build/provisioner_paused_before_retry.pkr.hcl create mode 100644 hcl2template/testdata/build/provisioner_timeout.pkr.hcl create mode 100644 packer/test-fixtures/build-prov-retry.json create mode 100644 provisioner/shell-local/provisioner_acc_test.go create mode 100644 provisioner/shell-local/test-fixtures/script.sh create mode 100644 provisioner/shell-local/test-fixtures/shell-local-provisioner.txt create mode 100644 template/test-fixtures/parse-provisioner-retry.json diff --git a/.circleci/config.yml b/.circleci/config.yml index 27884d8cb..bb8b45b07 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -51,6 +51,7 @@ commands: jobs: test-linux: executor: golang + resource_class: large working_directory: /go/src/github.com/hashicorp/packer steps: - checkout diff --git a/hcl2template/common_test.go b/hcl2template/common_test.go index ff2a94946..d6b9462aa 100644 --- a/hcl2template/common_test.go +++ b/hcl2template/common_test.go @@ -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{ { Key: "first_tag_key", diff --git a/hcl2template/testdata/build/provisioner_paused_before_retry.pkr.hcl b/hcl2template/testdata/build/provisioner_paused_before_retry.pkr.hcl new file mode 100644 index 000000000..04a8bbb81 --- /dev/null +++ b/hcl2template/testdata/build/provisioner_paused_before_retry.pkr.hcl @@ -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" { +} \ No newline at end of file diff --git a/hcl2template/testdata/build/provisioner_timeout.pkr.hcl b/hcl2template/testdata/build/provisioner_timeout.pkr.hcl new file mode 100644 index 000000000..477951060 --- /dev/null +++ b/hcl2template/testdata/build/provisioner_timeout.pkr.hcl @@ -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" { +} \ No newline at end of file diff --git a/hcl2template/types.build.provisioners.go b/hcl2template/types.build.provisioners.go index 49fbb5c42..706f154a0 100644 --- a/hcl2template/types.build.provisioners.go +++ b/hcl2template/types.build.provisioners.go @@ -2,6 +2,7 @@ package hcl2template import ( "fmt" + "time" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/gohcl" @@ -10,8 +11,11 @@ import ( // ProvisionerBlock references a detected but unparsed provisioner type ProvisionerBlock struct { - PType string - PName string + PType string + PName string + PauseBefore time.Duration + MaxRetries int + Timeout time.Duration HCL2Ref } @@ -21,17 +25,44 @@ func (p *ProvisionerBlock) String() string { func (p *Parser) decodeProvisioner(block *hcl.Block) (*ProvisionerBlock, hcl.Diagnostics) { var b struct { - Name string `hcl:"name,optional"` - Rest hcl.Body `hcl:",remain"` + Name string `hcl:"name,optional"` + 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) if diags.HasErrors() { return nil, diags } + provisioner := &ProvisionerBlock{ - PType: block.Labels[0], - PName: b.Name, - HCL2Ref: newHCL2Ref(block, b.Rest), + PType: block.Labels[0], + PName: b.Name, + 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) { diff --git a/hcl2template/types.packer_config.go b/hcl2template/types.packer_config.go index f6d4a99b6..fe8df0732 100644 --- a/hcl2template/types.packer_config.go +++ b/hcl2template/types.packer_config.go @@ -195,6 +195,26 @@ func (p *Parser) getCoreBuildProvisioners(source *SourceBlock, blocks []*Provisi if moreDiags.HasErrors() { 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{ PType: pb.PType, PName: pb.PName, diff --git a/hcl2template/types.packer_config_test.go b/hcl2template/types.packer_config_test.go index ae0b5030d..d3f1afce9 100644 --- a/hcl2template/types.packer_config_test.go +++ b/hcl2template/types.packer_config_test.go @@ -1,7 +1,9 @@ package hcl2template import ( + "path/filepath" "testing" + "time" "github.com/hashicorp/packer/packer" "github.com/zclconf/go-cty/cty" @@ -173,6 +175,90 @@ func TestParser_complete(t *testing.T) { parseWantDiags: 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) } diff --git a/packer/builder_mock.hcl2spec.go b/packer/builder_mock.hcl2spec.go index a5ea27c82..b90953d36 100644 --- a/packer/builder_mock.hcl2spec.go +++ b/packer/builder_mock.hcl2spec.go @@ -155,6 +155,7 @@ type FlatMockProvisioner struct { PrepCalled *bool `cty:"prep_called"` PrepConfigs []interface{} `cty:"prep_configs"` ProvCalled *bool `cty:"prov_called"` + ProvRetried *bool `cty:"prov_retried"` ProvCommunicator Communicator `cty:"prov_communicator"` 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_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_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_ui": &hcldec.AttrSpec{Name: "prov_ui", Type: cty.Bool, Required: false}, /* TODO(azr): could not find type */ } diff --git a/packer/core.go b/packer/core.go index 5d7337e61..05cb720ff 100644 --- a/packer/core.go +++ b/packer/core.go @@ -170,6 +170,12 @@ func (c *Core) generateCoreBuildProvisioner(rawP *template.Provisioner, rawName Provisioner: provisioner, } } + if rawP.MaxRetries != 0 { + provisioner = &RetriedProvisioner{ + MaxRetries: rawP.MaxRetries, + Provisioner: provisioner, + } + } cbp = CoreBuildProvisioner{ PType: rawP.Type, Provisioner: provisioner, diff --git a/packer/core_test.go b/packer/core_test.go index 17d76eb0c..90b8409cb 100644 --- a/packer/core_test.go +++ b/packer/core_test.go @@ -2,6 +2,7 @@ package packer import ( "context" + "errors" "os" "path/filepath" "reflect" @@ -786,3 +787,42 @@ func testCoreTemplate(t *testing.T, c *CoreConfig, p string) { 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") + } +} diff --git a/packer/provisioner.go b/packer/provisioner.go index 7b7fdaa60..bbda1bdf8 100644 --- a/packer/provisioner.go +++ b/packer/provisioner.go @@ -162,6 +162,48 @@ func (p *PausedProvisioner) Provision(ctx context.Context, ui Ui, comm Communica 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 // press before the provisioner is actually run. type DebuggedProvisioner struct { diff --git a/packer/provisioner_mock.go b/packer/provisioner_mock.go index bfc52aa88..9b17228e3 100644 --- a/packer/provisioner_mock.go +++ b/packer/provisioner_mock.go @@ -14,6 +14,7 @@ type MockProvisioner struct { PrepCalled bool PrepConfigs []interface{} ProvCalled bool + ProvRetried bool ProvCommunicator Communicator 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 { + if t.ProvCalled { + t.ProvRetried = true + return nil + } + t.ProvCalled = true t.ProvCommunicator = comm t.ProvUi = ui diff --git a/packer/provisioner_test.go b/packer/provisioner_test.go index 108dac90f..47addda71 100644 --- a/packer/provisioner_test.go +++ b/packer/provisioner_test.go @@ -2,6 +2,7 @@ package packer import ( "context" + "errors" "fmt" "testing" "time" @@ -229,3 +230,114 @@ func TestDebuggedProvisionerCancel(t *testing.T) { 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") + } +} diff --git a/packer/test-fixtures/build-prov-retry.json b/packer/test-fixtures/build-prov-retry.json new file mode 100644 index 000000000..e2128855f --- /dev/null +++ b/packer/test-fixtures/build-prov-retry.json @@ -0,0 +1,10 @@ +{ + "builders": [{ + "type": "test" + }], + + "provisioners": [{ + "type": "test", + "max_retries": 1 + }] +} diff --git a/provisioner/shell-local/provisioner_acc_test.go b/provisioner/shell-local/provisioner_acc_test.go new file mode 100644 index 000000000..e9e7d4f31 --- /dev/null +++ b/provisioner/shell-local/provisioner_acc_test.go @@ -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 +} diff --git a/provisioner/shell-local/test-fixtures/script.sh b/provisioner/shell-local/test-fixtures/script.sh new file mode 100644 index 000000000..77637c719 --- /dev/null +++ b/provisioner/shell-local/test-fixtures/script.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +if [[ ! -f file.txt ]] ; then + echo 'hello' > file.txt + exit 1 +fi \ No newline at end of file diff --git a/provisioner/shell-local/test-fixtures/shell-local-provisioner.txt b/provisioner/shell-local/test-fixtures/shell-local-provisioner.txt new file mode 100644 index 000000000..37929adeb --- /dev/null +++ b/provisioner/shell-local/test-fixtures/shell-local-provisioner.txt @@ -0,0 +1,5 @@ +{ + "type": "shell-local", + "script": "test-fixtures/script.sh", + "max_retries" : 5 +} \ No newline at end of file diff --git a/template/parse.go b/template/parse.go index 2c2cf8b24..3f1ac7e67 100644 --- a/template/parse.go +++ b/template/parse.go @@ -76,6 +76,7 @@ func (r *rawTemplate) decodeProvisioner(raw interface{}) (Provisioner, error) { delete(p.Config, "only") delete(p.Config, "override") delete(p.Config, "pause_before") + delete(p.Config, "max_retries") delete(p.Config, "type") delete(p.Config, "timeout") diff --git a/template/parse_test.go b/template/parse_test.go index 4c2afbbeb..0b25e973b 100644 --- a/template/parse_test.go +++ b/template/parse_test.go @@ -106,6 +106,19 @@ func TestParse(t *testing.T) { false, }, + { + "parse-provisioner-retry.json", + &Template{ + Provisioners: []*Provisioner{ + { + Type: "something", + MaxRetries: 5, + }, + }, + }, + false, + }, + { "parse-provisioner-timeout.json", &Template{ diff --git a/template/template.go b/template/template.go index 6bab8d4c2..6d810a656 100644 --- a/template/template.go +++ b/template/template.go @@ -141,6 +141,7 @@ type Provisioner struct { Config map[string]interface{} `json:"config,omitempty"` Override map[string]interface{} `json:"override,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"` } diff --git a/template/template.hcl2spec.go b/template/template.hcl2spec.go index ee93d2013..17c164221 100644 --- a/template/template.hcl2spec.go +++ b/template/template.hcl2spec.go @@ -15,6 +15,7 @@ type FlatProvisioner struct { Config map[string]interface{} `json:"config,omitempty" cty:"config"` Override map[string]interface{} `json:"override,omitempty" cty:"override"` 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"` } @@ -36,6 +37,7 @@ func (*FlatProvisioner) HCL2Spec() map[string]hcldec.Spec { "config": &hcldec.AttrSpec{Name: "config", 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}, + "max_retries": &hcldec.AttrSpec{Name: "max_retries", Type: cty.Number, Required: false}, "timeout": &hcldec.AttrSpec{Name: "timeout", Type: cty.String, Required: false}, } return s diff --git a/template/test-fixtures/parse-provisioner-retry.json b/template/test-fixtures/parse-provisioner-retry.json new file mode 100644 index 000000000..3fdc3aff3 --- /dev/null +++ b/template/test-fixtures/parse-provisioner-retry.json @@ -0,0 +1,8 @@ +{ + "provisioners": [ + { + "type": "something", + "max_retries": 5 + } + ] +} diff --git a/website/pages/docs/templates/provisioners.mdx b/website/pages/docs/templates/provisioners.mdx index a265ff6c4..bf1dd2d73 100644 --- a/website/pages/docs/templates/provisioners.mdx +++ b/website/pages/docs/templates/provisioners.mdx @@ -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 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 Sometimes a command can take much more time than expected diff --git a/website/pages/partials/provisioners/common-config.mdx b/website/pages/partials/provisioners/common-config.mdx index fcaed8901..d819686fc 100644 --- a/website/pages/partials/provisioners/common-config.mdx +++ b/website/pages/partials/provisioners/common-config.mdx @@ -2,6 +2,8 @@ Parameters common to all provisioners: - `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) by name.