Add -on-error command line argument to allow preserving artifacts on builder errors

Resolves #409
This commit is contained in:
Orivej Desh 2016-09-14 00:04:18 +00:00
parent d310de8cf5
commit 6762965696
23 changed files with 231 additions and 170 deletions

View File

@ -262,15 +262,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
)
// Run!
if b.config.PackerDebug {
b.runner = &multistep.DebugRunner{
Steps: steps,
PauseFn: common.MultistepDebugFn(ui),
}
} else {
b.runner = &multistep.BasicRunner{Steps: steps}
}
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
b.runner.Run(state)
// If there was an error, return that

View File

@ -179,15 +179,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}
// Run!
if b.config.PackerDebug {
b.runner = &multistep.DebugRunner{
Steps: steps,
PauseFn: common.MultistepDebugFn(ui),
}
} else {
b.runner = &multistep.BasicRunner{Steps: steps}
}
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
b.runner.Run(state)
// If there was an error, return that

View File

@ -262,15 +262,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}
// Run!
if b.config.PackerDebug {
b.runner = &multistep.DebugRunner{
Steps: steps,
PauseFn: common.MultistepDebugFn(ui),
}
} else {
b.runner = &multistep.BasicRunner{Steps: steps}
}
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
b.runner.Run(state)
// If there was an error, return that

View File

@ -157,7 +157,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
ui.Message(fmt.Sprintf("temp admin password: '%s'", b.config.Password))
}
b.runner = b.createRunner(&steps, ui)
b.runner = packerCommon.NewRunner(steps, b.config.PackerConfig, ui)
b.runner.Run(b.stateBag)
// Report any errors.
@ -198,19 +198,6 @@ func (b *Builder) Cancel() {
}
}
func (b *Builder) createRunner(steps *[]multistep.Step, ui packer.Ui) multistep.Runner {
if b.config.PackerDebug {
return &multistep.DebugRunner{
Steps: *steps,
PauseFn: packerCommon.MultistepDebugFn(ui),
}
}
return &multistep.BasicRunner{
Steps: *steps,
}
}
func (b *Builder) getBlobEndpoint(client *AzureClient, resourceGroupName string, storageAccountName string) (string, error) {
account, err := client.AccountsClient.GetProperties(resourceGroupName, storageAccountName)
if err != nil {

View File

@ -73,15 +73,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}
// Run the steps
if b.config.PackerDebug {
b.runner = &multistep.DebugRunner{
Steps: steps,
PauseFn: common.MultistepDebugFn(ui),
}
} else {
b.runner = &multistep.BasicRunner{Steps: steps}
}
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
b.runner.Run(state)
// If there was an error, return that

View File

@ -78,15 +78,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
state.Put("driver", driver)
// Run!
if b.config.PackerDebug {
b.runner = &multistep.DebugRunner{
Steps: steps,
PauseFn: common.MultistepDebugFn(ui),
}
} else {
b.runner = &multistep.BasicRunner{Steps: steps}
}
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
b.runner.Run(state)
// If there was an error, return that

View File

@ -73,14 +73,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}
// Run the steps.
if b.config.PackerDebug {
b.runner = &multistep.DebugRunner{
Steps: steps,
PauseFn: common.MultistepDebugFn(ui),
}
} else {
b.runner = &multistep.BasicRunner{Steps: steps}
}
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
b.runner.Run(state)
// Report any errors.

View File

@ -46,15 +46,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
state.Put("ui", ui)
// Run!
if b.config.PackerDebug {
b.runner = &multistep.DebugRunner{
Steps: steps,
PauseFn: common.MultistepDebugFn(ui),
}
} else {
b.runner = &multistep.BasicRunner{Steps: steps}
}
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
b.runner.Run(state)
// If there was an error, return that

View File

@ -116,15 +116,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}
// Run!
if b.config.PackerDebug {
b.runner = &multistep.DebugRunner{
Steps: steps,
PauseFn: common.MultistepDebugFn(ui),
}
} else {
b.runner = &multistep.BasicRunner{Steps: steps}
}
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
b.runner.Run(state)
// If there was an error, return that

View File

@ -215,17 +215,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
state.Put("ui", ui)
// Run
if b.config.PackerDebug {
pauseFn := common.MultistepDebugFn(ui)
state.Put("pauseFn", pauseFn)
b.runner = &multistep.DebugRunner{
Steps: steps,
PauseFn: pauseFn,
}
} else {
b.runner = &multistep.BasicRunner{Steps: steps}
}
b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state)
b.runner.Run(state)
// If there was an error, return that

View File

@ -108,16 +108,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}
// Run the steps.
if b.config.PackerDebug {
pauseFn := common.MultistepDebugFn(ui)
state.Put("pauseFn", pauseFn)
b.runner = &multistep.DebugRunner{
Steps: steps,
PauseFn: pauseFn,
}
} else {
b.runner = &multistep.BasicRunner{Steps: steps}
}
b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state)
b.runner.Run(state)
// Report any errors.

View File

@ -422,17 +422,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
state.Put("ui", ui)
// Run
if b.config.PackerDebug {
pauseFn := common.MultistepDebugFn(ui)
state.Put("pauseFn", pauseFn)
b.runner = &multistep.DebugRunner{
Steps: steps,
PauseFn: pauseFn,
}
} else {
b.runner = &multistep.BasicRunner{Steps: steps}
}
b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state)
b.runner.Run(state)
// If there was an error, return that

View File

@ -275,17 +275,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
state.Put("ui", ui)
// Run
if b.config.PackerDebug {
pauseFn := common.MultistepDebugFn(ui)
state.Put("pauseFn", pauseFn)
b.runner = &multistep.DebugRunner{
Steps: steps,
PauseFn: pauseFn,
}
} else {
b.runner = &multistep.BasicRunner{Steps: steps}
}
b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state)
b.runner.Run(state)
// If there was an error, return that

View File

@ -135,16 +135,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}
// Run the steps.
if b.config.PackerDebug {
pauseFn := common.MultistepDebugFn(ui)
state.Put("pauseFn", pauseFn)
b.runner = &multistep.DebugRunner{
Steps: steps,
PauseFn: pauseFn,
}
} else {
b.runner = &multistep.BasicRunner{Steps: steps}
}
b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state)
b.runner.Run(state)
// Report any errors.

View File

@ -305,17 +305,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}
// Run!
if b.config.PackerDebug {
pauseFn := common.MultistepDebugFn(ui)
state.Put("pauseFn", pauseFn)
b.runner = &multistep.DebugRunner{
Steps: steps,
PauseFn: pauseFn,
}
} else {
b.runner = &multistep.BasicRunner{Steps: steps}
}
b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state)
b.runner.Run(state)
// If there was an error, return that

View File

@ -122,16 +122,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}
// Run the steps.
if b.config.PackerDebug {
pauseFn := common.MultistepDebugFn(ui)
state.Put("pauseFn", pauseFn)
b.runner = &multistep.DebugRunner{
Steps: steps,
PauseFn: pauseFn,
}
} else {
b.runner = &multistep.BasicRunner{Steps: steps}
}
b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state)
b.runner.Run(state)
// Report any errors.

View File

@ -20,11 +20,13 @@ type BuildCommand struct {
func (c BuildCommand) Run(args []string) int {
var cfgColor, cfgDebug, cfgForce, cfgParallel bool
var cfgOnError string
flags := c.Meta.FlagSet("build", FlagSetBuildFilter|FlagSetVars)
flags.Usage = func() { c.Ui.Say(c.Help()) }
flags.BoolVar(&cfgColor, "color", true, "")
flags.BoolVar(&cfgDebug, "debug", false, "")
flags.BoolVar(&cfgForce, "force", false, "")
flags.StringVar(&cfgOnError, "on-error", "cleanup", "")
flags.BoolVar(&cfgParallel, "parallel", true, "")
if err := flags.Parse(args); err != nil {
return 1
@ -99,12 +101,14 @@ func (c BuildCommand) Run(args []string) int {
log.Printf("Build debug mode: %v", cfgDebug)
log.Printf("Force build: %v", cfgForce)
log.Printf("On error: %v", cfgOnError)
// Set the debug and force mode and prepare all the builds
for _, b := range builds {
log.Printf("Preparing build: %s", b.Name())
b.SetDebug(cfgDebug)
b.SetForce(cfgForce)
b.SetOnError(cfgOnError)
warnings, err := b.Prepare()
if err != nil {
@ -284,6 +288,9 @@ Options:
-except=foo,bar,baz Build all builds other than these
-force Force a build to continue if artifacts exist, deletes existing artifacts
-machine-readable Machine-readable output
-on-error=abort When a builder fails, abort without cleanup
-on-error=ask When a builder fails, prompt for action
-on-error=cleanup When a builder fails, clean up and exit (the default)
-only=foo,bar,baz Only build the given builds by name
-parallel=false Disable parallelization (on by default)
-var 'key=value' Variable for templates, can be used multiple times.

148
common/multistep_runner.go Normal file
View File

@ -0,0 +1,148 @@
package common
import (
"fmt"
"log"
"os"
"reflect"
"time"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
func newRunner(steps []multistep.Step, config PackerConfig, ui packer.Ui) (multistep.Runner, interface{}) {
switch config.PackerOnError {
case "cleanup", "":
case "abort":
for i, step := range steps {
steps[i] = abortStep{step, ui}
}
case "ask":
for i, step := range steps {
steps[i] = askStep{step, ui}
}
default:
ui.Error(fmt.Sprintf("Ignoring unknown on-error value %q", config.PackerOnError))
}
if config.PackerDebug {
pauseFn := MultistepDebugFn(ui)
return &multistep.DebugRunner{Steps: steps, PauseFn: pauseFn}, pauseFn
} else {
return &multistep.BasicRunner{Steps: steps}, nil
}
}
func NewRunner(steps []multistep.Step, config PackerConfig, ui packer.Ui) multistep.Runner {
runner, _ := newRunner(steps, config, ui)
return runner
}
func NewRunnerWithPauseFn(steps []multistep.Step, config PackerConfig, ui packer.Ui, state multistep.StateBag) multistep.Runner {
runner, pauseFn := newRunner(steps, config, ui)
if pauseFn != nil {
state.Put("pauseFn", pauseFn)
}
return runner
}
func typeName(i interface{}) string {
return reflect.Indirect(reflect.ValueOf(i)).Type().Name()
}
type abortStep struct {
step multistep.Step
ui packer.Ui
}
func (s abortStep) Run(state multistep.StateBag) multistep.StepAction {
return s.step.Run(state)
}
func (s abortStep) Cleanup(state multistep.StateBag) {
if _, ok := state.GetOk(multistep.StateCancelled); ok {
s.ui.Error("Interrupted, aborting...")
os.Exit(1)
}
if _, ok := state.GetOk(multistep.StateHalted); ok {
s.ui.Error(fmt.Sprintf("Step %q failed, aborting...", typeName(s.step)))
os.Exit(1)
}
s.step.Cleanup(state)
}
type askStep struct {
step multistep.Step
ui packer.Ui
}
func (s askStep) Run(state multistep.StateBag) (action multistep.StepAction) {
for {
action = s.step.Run(state)
if action != multistep.ActionHalt {
return
}
switch ask(s.ui, typeName(s.step), state) {
case askCleanup:
return
case askAbort:
os.Exit(1)
case askRetry:
continue
}
}
}
func (s askStep) Cleanup(state multistep.StateBag) {
s.step.Cleanup(state)
}
type askResponse int
const (
askCleanup askResponse = iota
askAbort
askRetry
)
func ask(ui packer.Ui, name string, state multistep.StateBag) askResponse {
ui.Say(fmt.Sprintf("Step %q failed", name))
result := make(chan askResponse)
go func() {
result <- askPrompt(ui)
}()
for {
select {
case response := <-result:
return response
case <-time.After(100 * time.Millisecond):
if _, ok := state.GetOk(multistep.StateCancelled); ok {
return askCleanup
}
}
}
}
func askPrompt(ui packer.Ui) askResponse {
for {
line, err := ui.Ask("[C]lean up and exit, [A]bort without cleanup, or [R]etry step (build may fail even if retry succeeds)? [car]")
if err != nil {
log.Printf("Error asking for input: %s", err)
}
switch {
case len(line) == 0 || line[0] == 'c':
return askCleanup
case line[0] == 'a':
return askAbort
case line[0] == 'r':
return askRetry
}
ui.Say(fmt.Sprintf("Incorrect input: %#v", line))
}
}

View File

@ -8,5 +8,6 @@ type PackerConfig struct {
PackerBuilderType string `mapstructure:"packer_builder_type"`
PackerDebug bool `mapstructure:"packer_debug"`
PackerForce bool `mapstructure:"packer_force"`
PackerOnError string `mapstructure:"packer_on_error"`
PackerUserVars map[string]string `mapstructure:"packer_user_variables"`
}

View File

@ -24,6 +24,12 @@ const (
// force build is enabled.
ForceConfigKey = "packer_force"
// This key determines what to do when a normal multistep step fails
// - "cleanup" - run cleanup steps
// - "abort" - exit without cleanup
// - "ask" - ask the user
OnErrorConfigKey = "packer_on_error"
// TemplatePathKey is the path to the template that configured this build
TemplatePathKey = "packer_template_path"
@ -67,6 +73,12 @@ type Build interface {
// When SetForce is set to true, existing artifacts from the build are
// deleted prior to the build.
SetForce(bool)
// SetOnError will determines what to do when a normal multistep step fails
// - "cleanup" - run cleanup steps
// - "abort" - exit without cleanup
// - "ask" - ask the user
SetOnError(string)
}
// A build struct represents a single build job, the result of which should
@ -86,6 +98,7 @@ type coreBuild struct {
debug bool
force bool
onError string
l sync.Mutex
prepareCalled bool
}
@ -129,6 +142,7 @@ func (b *coreBuild) Prepare() (warn []string, err error) {
BuilderTypeConfigKey: b.builderType,
DebugConfigKey: b.debug,
ForceConfigKey: b.force,
OnErrorConfigKey: b.onError,
TemplatePathKey: b.templatePath,
UserVariablesConfigKey: b.variables,
}
@ -306,6 +320,14 @@ func (b *coreBuild) SetForce(val bool) {
b.force = val
}
func (b *coreBuild) SetOnError(val string) {
if b.prepareCalled {
panic("prepare has already been called")
}
b.onError = val
}
// Cancels the build if it is running.
func (b *coreBuild) Cancel() {
b.builder.Cancel()

View File

@ -23,6 +23,7 @@ func testBuild() *coreBuild {
},
},
variables: make(map[string]string),
onError: "cleanup",
}
}
@ -32,6 +33,7 @@ func testDefaultPackerConfig() map[string]interface{} {
BuilderTypeConfigKey: "foo",
DebugConfigKey: false,
ForceConfigKey: false,
OnErrorConfigKey: "cleanup",
TemplatePathKey: "",
UserVariablesConfigKey: make(map[string]string),
}

View File

@ -1,8 +1,9 @@
package rpc
import (
"github.com/mitchellh/packer/packer"
"net/rpc"
"github.com/mitchellh/packer/packer"
)
// An implementation of packer.Build where the build is actually executed
@ -79,6 +80,12 @@ func (b *build) SetForce(val bool) {
}
}
func (b *build) SetOnError(val string) {
if err := b.client.Call("Build.SetOnError", val, new(interface{})); err != nil {
panic(err)
}
}
func (b *build) Cancel() {
if err := b.client.Call("Build.Cancel", new(interface{}), new(interface{})); err != nil {
panic(err)
@ -134,6 +141,11 @@ func (b *BuildServer) SetForce(val *bool, reply *interface{}) error {
return nil
}
func (b *BuildServer) SetOnError(val *string, reply *interface{}) error {
b.build.SetOnError(*val)
return nil
}
func (b *BuildServer) Cancel(args *interface{}, reply *interface{}) error {
b.build.Cancel()
return nil

View File

@ -2,23 +2,25 @@ package rpc
import (
"errors"
"github.com/mitchellh/packer/packer"
"reflect"
"testing"
"github.com/mitchellh/packer/packer"
)
var testBuildArtifact = &packer.MockArtifact{}
type testBuild struct {
nameCalled bool
prepareCalled bool
prepareWarnings []string
runCalled bool
runCache packer.Cache
runUi packer.Ui
setDebugCalled bool
setForceCalled bool
cancelCalled bool
nameCalled bool
prepareCalled bool
prepareWarnings []string
runCalled bool
runCache packer.Cache
runUi packer.Ui
setDebugCalled bool
setForceCalled bool
setOnErrorCalled bool
cancelCalled bool
errRunResult bool
}
@ -53,6 +55,10 @@ func (b *testBuild) SetForce(bool) {
b.setForceCalled = true
}
func (b *testBuild) SetOnError(string) {
b.setOnErrorCalled = true
}
func (b *testBuild) Cancel() {
b.cancelCalled = true
}
@ -116,6 +122,12 @@ func TestBuild(t *testing.T) {
t.Fatal("should be called")
}
// Test SetOnError
bClient.SetOnError("ask")
if !b.setOnErrorCalled {
t.Fatal("should be called")
}
// Test Cancel
bClient.Cancel()
if !b.cancelCalled {