From 5bd8fee7083aa3cf5dedbbe69396e6cfbe837d08 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Tue, 24 Sep 2019 09:44:19 -0700 Subject: [PATCH] Creates a final "cleanup" provisioner to run if an error occurs during a provisioning step, allowing users to perform any custom cleanup tasks that must happen on the VM before the VM is shut down and destroyed. --- common/step_provision.go | 35 +++++++++++-- packer/build.go | 41 +++++++++++---- packer/core.go | 107 +++++++++++++++++++++++---------------- packer/hook.go | 1 + template/parse.go | 34 +++++++++++++ template/template.go | 1 + 6 files changed, 162 insertions(+), 57 deletions(-) diff --git a/common/step_provision.go b/common/step_provision.go index dfa756b72..2c13b37d5 100644 --- a/common/step_provision.go +++ b/common/step_provision.go @@ -2,6 +2,7 @@ package common import ( "context" + "fmt" "log" "time" @@ -22,7 +23,8 @@ type StepProvision struct { Comm packer.Communicator } -func (s *StepProvision) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { +func (s *StepProvision) runWithHook(ctx context.Context, state multistep.StateBag, hooktype string) multistep.StepAction { + // hooktype will be either packer.HookProvision or packer.HookCleanupProvision comm := s.Comm if comm == nil { raw, ok := state.Get("communicator").(packer.Communicator) @@ -35,17 +37,29 @@ func (s *StepProvision) Run(ctx context.Context, state multistep.StateBag) multi // Run the provisioner in a goroutine so we can continually check // for cancellations... - log.Println("Running the provision hook") + if hooktype == packer.HookProvision { + log.Println("Running the provision hook") + } else if hooktype == packer.HookCleanupProvision { + ui.Say("Provisioning step had errors: Running the cleanup provisioner, if present...") + } errCh := make(chan error, 1) go func() { - errCh <- hook.Run(ctx, packer.HookProvision, ui, comm, nil) + errCh <- hook.Run(ctx, hooktype, ui, comm, nil) }() for { select { case err := <-errCh: if err != nil { - state.Put("error", err) + if hooktype == packer.HookProvision { + // We don't overwrite the error if it's a cleanup + // provisioner being run. + state.Put("error", err) + } else if hooktype == packer.HookCleanupProvision { + origErr := state.Get("error").(error) + state.Put("error", fmt.Errorf("Cleanup failed: %s. "+ + "Original Provisioning error: %s", err, origErr)) + } return multistep.ActionHalt } @@ -62,4 +76,15 @@ func (s *StepProvision) Run(ctx context.Context, state multistep.StateBag) multi } } -func (*StepProvision) Cleanup(multistep.StateBag) {} +func (s *StepProvision) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + return s.runWithHook(ctx, state, packer.HookProvision) +} + +func (s *StepProvision) Cleanup(state multistep.StateBag) { + // We have a "final" provisioner that gets defined by "on-error-script" + // which we only call if there's an error during the provision run and + // the "on-error-script" is defined. + if _, ok := state.GetOk("error"); ok { + s.runWithHook(context.Background(), state, packer.HookCleanupProvision) + } +} diff --git a/packer/build.go b/packer/build.go index 649ef42a5..badd67c1f 100644 --- a/packer/build.go +++ b/packer/build.go @@ -84,15 +84,16 @@ type Build interface { // multiple files, of course, but it should be for only a single provider // (such as VirtualBox, EC2, etc.). type coreBuild struct { - name string - builder Builder - builderConfig interface{} - builderType string - hooks map[string][]Hook - postProcessors [][]coreBuildPostProcessor - provisioners []coreBuildProvisioner - templatePath string - variables map[string]string + name string + builder Builder + builderConfig interface{} + builderType string + hooks map[string][]Hook + postProcessors [][]coreBuildPostProcessor + provisioners []coreBuildProvisioner + cleanupProvisioner coreBuildProvisioner + templatePath string + variables map[string]string debug bool force bool @@ -164,6 +165,17 @@ func (b *coreBuild) Prepare() (warn []string, err error) { } } + // Prepare the on-error-cleanup provisioner + if b.cleanupProvisioner.pType != "" { + configs := make([]interface{}, len(b.cleanupProvisioner.config), len(b.cleanupProvisioner.config)+1) + copy(configs, b.cleanupProvisioner.config) + configs = append(configs, packerConfig) + err = b.cleanupProvisioner.provisioner.Prepare(configs...) + if err != nil { + return + } + } + // Prepare the post-processors for _, ppSeq := range b.postProcessors { for _, corePP := range ppSeq { @@ -222,6 +234,17 @@ func (b *coreBuild) Run(ctx context.Context, originalUi Ui) ([]Artifact, error) }) } + if b.cleanupProvisioner.pType != "" { + hookedCleanupProvisioner := &HookedProvisioner{ + b.cleanupProvisioner.provisioner, + b.cleanupProvisioner.config, + b.cleanupProvisioner.pType, + } + hooks[HookCleanupProvision] = []Hook{&ProvisionHook{ + Provisioners: []*HookedProvisioner{hookedCleanupProvisioner}, + }} + } + hook := &DispatchHook{Mapping: hooks} artifacts := make([]Artifact, 0, 1) diff --git a/packer/core.go b/packer/core.go index 3e9895dcb..bcdd91b30 100644 --- a/packer/core.go +++ b/packer/core.go @@ -112,6 +112,49 @@ func (c *Core) BuildNames() []string { return r } +func (c *Core) generateCoreBuildProvisioner(rawP *template.Provisioner, rawName string) (coreBuildProvisioner, error) { + // Get the provisioner + cbp := coreBuildProvisioner{} + provisioner, err := c.components.Provisioner(rawP.Type) + if err != nil { + return cbp, fmt.Errorf( + "error initializing provisioner '%s': %s", + rawP.Type, err) + } + if provisioner == nil { + return cbp, fmt.Errorf( + "provisioner type not found: %s", rawP.Type) + } + + // Get the configuration + config := make([]interface{}, 1, 2) + config[0] = rawP.Config + if rawP.Override != nil { + if override, ok := rawP.Override[rawName]; ok { + config = append(config, override) + } + } + // If we're pausing, we wrap the provisioner in a special pauser. + if rawP.PauseBefore > 0 { + provisioner = &PausedProvisioner{ + PauseBefore: rawP.PauseBefore, + Provisioner: provisioner, + } + } else if rawP.Timeout > 0 { + provisioner = &TimeoutProvisioner{ + Timeout: rawP.Timeout, + Provisioner: provisioner, + } + } + cbp = coreBuildProvisioner{ + pType: rawP.Type, + provisioner: provisioner, + config: config, + } + + return cbp, nil +} + // Build returns the Build object for the given name. func (c *Core) Build(n string) (Build, error) { // Setup the builder @@ -140,46 +183,23 @@ func (c *Core) Build(n string) (Build, error) { if rawP.OnlyExcept.Skip(rawName) { continue } - - // Get the provisioner - provisioner, err := c.components.Provisioner(rawP.Type) + cbp, err := c.generateCoreBuildProvisioner(rawP, rawName) if err != nil { - return nil, fmt.Errorf( - "error initializing provisioner '%s': %s", - rawP.Type, err) - } - if provisioner == nil { - return nil, fmt.Errorf( - "provisioner type not found: %s", rawP.Type) + return nil, err } - // Get the configuration - config := make([]interface{}, 1, 2) - config[0] = rawP.Config - if rawP.Override != nil { - if override, ok := rawP.Override[rawName]; ok { - config = append(config, override) - } - } + provisioners = append(provisioners, cbp) + } - // If we're pausing, we wrap the provisioner in a special pauser. - if rawP.PauseBefore > 0 { - provisioner = &PausedProvisioner{ - PauseBefore: rawP.PauseBefore, - Provisioner: provisioner, - } - } else if rawP.Timeout > 0 { - provisioner = &TimeoutProvisioner{ - Timeout: rawP.Timeout, - Provisioner: provisioner, - } + var cleanupProvisioner coreBuildProvisioner + if c.Template.CleanupProvisioner != nil { + // This is a special instantiation of the shell-local provisioner that + // is only run on error at end of provisioning step before other step + // cleanup occurs. + cleanupProvisioner, err = c.generateCoreBuildProvisioner(c.Template.CleanupProvisioner, rawName) + if err != nil { + return nil, err } - - provisioners = append(provisioners, coreBuildProvisioner{ - pType: rawP.Type, - provisioner: provisioner, - config: config, - }) } // Setup the post-processors @@ -232,14 +252,15 @@ func (c *Core) Build(n string) (Build, error) { // TODO hooks one day return &coreBuild{ - name: n, - builder: builder, - builderConfig: configBuilder.Config, - builderType: configBuilder.Type, - postProcessors: postProcessors, - provisioners: provisioners, - templatePath: c.Template.Path, - variables: c.variables, + name: n, + builder: builder, + builderConfig: configBuilder.Config, + builderType: configBuilder.Type, + postProcessors: postProcessors, + provisioners: provisioners, + cleanupProvisioner: cleanupProvisioner, + templatePath: c.Template.Path, + variables: c.variables, }, nil } diff --git a/packer/hook.go b/packer/hook.go index 67074aa7d..a0b146023 100644 --- a/packer/hook.go +++ b/packer/hook.go @@ -6,6 +6,7 @@ import ( // This is the hook that should be fired for provisioners to run. const HookProvision = "packer_provision" +const HookCleanupProvision = "packer_cleanup_provision" // A Hook is used to hook into an arbitrarily named location in a build, // allowing custom behavior to run at certain points along a build. diff --git a/template/parse.go b/template/parse.go index cd4467b8f..dbc812198 100644 --- a/template/parse.go +++ b/template/parse.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "io" + "log" "os" "path/filepath" "sort" @@ -28,6 +29,7 @@ type rawTemplate struct { Push map[string]interface{} `json:"push,omitempty"` PostProcessors []interface{} `mapstructure:"post-processors" json:"post-processors,omitempty"` Provisioners []interface{} `json:"provisioners,omitempty"` + CleanupProvisioner interface{} `mapstructure:"on-error-script" json:"on-error-script,omitempty"` Variables map[string]interface{} `json:"variables,omitempty"` SensitiveVariables []string `mapstructure:"sensitive-variables" json:"sensitive-variables,omitempty"` @@ -242,6 +244,38 @@ func (r *rawTemplate) Template() (*Template, error) { result.Provisioners = append(result.Provisioners, &p) } + // Gather the on-error-script + log.Printf("r.CleanupProvisioner is %#v", r.CleanupProvisioner) + if r.CleanupProvisioner != nil { + var p Provisioner + if err := r.decoder(&p, nil).Decode(r.CleanupProvisioner); err != nil { + errs = multierror.Append(errs, fmt.Errorf( + "On Error Cleanup provisioner: %s", err)) + } + + // Type is required before any richer validation + log.Printf("p is %#v", p) + if p.Type == "" { + errs = multierror.Append(errs, fmt.Errorf( + "on error cleanup provisioner missing 'type'")) + } + + // Set the raw configuration and delete any special keys + p.Config = r.CleanupProvisioner.(map[string]interface{}) + + delete(p.Config, "except") + delete(p.Config, "only") + delete(p.Config, "override") + delete(p.Config, "pause_before") + delete(p.Config, "type") + delete(p.Config, "timeout") + + if len(p.Config) == 0 { + p.Config = nil + } + result.CleanupProvisioner = &p + } + // If we have errors, return those with a nil result if errs != nil { return nil, errs diff --git a/template/template.go b/template/template.go index 6013313de..aba88c7d0 100644 --- a/template/template.go +++ b/template/template.go @@ -24,6 +24,7 @@ type Template struct { SensitiveVariables []*Variable Builders map[string]*Builder Provisioners []*Provisioner + CleanupProvisioner *Provisioner PostProcessors [][]*PostProcessor // RawContents is just the raw data for this template