Add RetriedProvisioner to allow retry provisioners (#9061)
This commit is contained in:
parent
d580ea7950
commit
553b1fb9f8
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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" {
|
||||||
|
}
|
|
@ -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" {
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 */
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"builders": [{
|
||||||
|
"type": "test"
|
||||||
|
}],
|
||||||
|
|
||||||
|
"provisioners": [{
|
||||||
|
"type": "test",
|
||||||
|
"max_retries": 1
|
||||||
|
}]
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [[ ! -f file.txt ]] ; then
|
||||||
|
echo 'hello' > file.txt
|
||||||
|
exit 1
|
||||||
|
fi
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"type": "shell-local",
|
||||||
|
"script": "test-fixtures/script.sh",
|
||||||
|
"max_retries" : 5
|
||||||
|
}
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"provisioners": [
|
||||||
|
{
|
||||||
|
"type": "something",
|
||||||
|
"max_retries": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue