Skip to content

Commit

Permalink
Print pending actions during apply and destroy
Browse files Browse the repository at this point in the history
During `apply` and `destroy` commands, prints the current set of
resources if no apply step has started or stopped within ten seconds.

Fixes hashicorp#5932.

Note: this removes the wrapping of UiHook.Ui in a ConcurrentUi, because
it appears that it is already a ConcurrentUi when constructed in Meta
anyway. Since there are now two hooks that share the Ui, what's
important is that the underlying Ui passed to both hooks is concurrent;
the fact that UiHook's is itself concurrent is distracting.
  • Loading branch information
glasser committed Apr 11, 2016
1 parent 84b913b commit ade9d9e
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 11 deletions.
7 changes: 6 additions & 1 deletion command/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ func (c *ApplyCommand) Run(args []string) int {
// Prepare the extra hooks to count resources
countHook := new(CountHook)
stateHook := new(StateHook)
c.Meta.extraHooks = []terraform.Hook{countHook, stateHook}
pendingHook := &PendingHook{
Colorize: c.Colorize(),
Ui: c.Ui,
}
c.Meta.extraHooks = []terraform.Hook{countHook, stateHook, pendingHook}

if !c.Destroy && maybeInit {
// Do a detect to determine if we need to do an init + apply.
Expand Down Expand Up @@ -184,6 +188,7 @@ func (c *ApplyCommand) Run(args []string) int {
var state *terraform.State
var applyErr error
doneCh := make(chan struct{})
pendingHook.ShowPendingOperationsInBackground(doneCh)
go func() {
defer close(doneCh)
state, applyErr = ctx.Apply()
Expand Down
152 changes: 152 additions & 0 deletions command/hook_pending.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package command

import (
"fmt"
"sort"
"strings"
"time"

"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
"github.com/mitchellh/colorstring"
)

type pendingEvent struct {
// Kind of operation that started, or "" if the operation ended.
op string
// Human ID of the node being operated on.
id string
}

type PendingHook struct {
terraform.NilHook

Colorize *colorstring.Colorize
Ui cli.Ui

events chan *pendingEvent
}

func (h *PendingHook) PreApply(
n *terraform.InstanceInfo,
s *terraform.InstanceState,
d *terraform.InstanceDiff) (terraform.HookAction, error) {
op := "modifying"
if d.Destroy {
op = "destroying"
} else if s.ID == "" {
op = "creating"
}

h.events <- &pendingEvent{
op: op,
id: n.HumanId(),
}

return terraform.HookActionContinue, nil
}

func (h *PendingHook) PostApply(
n *terraform.InstanceInfo,
s *terraform.InstanceState,
applyerr error) (terraform.HookAction, error) {
h.events <- &pendingEvent{
id: n.HumanId(),
}

return terraform.HookActionContinue, nil
}

// We sometimes get multiple PreApply with the same ID (eg, if destroying
// multiple old versions of something?). Keep reference counts.
type resource struct {
op string
refs int
}

func (h *PendingHook) ShowPendingOperationsInBackground(doneCh <-chan struct{}) {
h.events = make(chan *pendingEvent)
pendingById := make(map[string]*resource)

go func() {
for {
select {
case <-doneCh:
// The apply is done; nothing more to print.
return

case event := <-h.events:
// Something happened! Update our internal state and restart the timer.
if event.op == "" {
// PostApply. Note that this can get called even if there's a no-op diff.
if r, found := pendingById[event.id]; found {
if r.refs == 0 {
delete(pendingById, event.id)
} else {
r.refs--
}
}
// ... otherwise ignore.
} else {
// PreApply. Note that it's possible for this to be called more than
// once for the same resource ID.
if r, found := pendingById[event.id]; found {
r.refs++
} else {
pendingById[event.id] = &resource{op: event.op, refs: 1}
}
}

case <-time.After(10 * time.Second):
// It's been a while. Print something.
h.outputPending(pendingById)
}
}
}()
}

func (h *PendingHook) outputPending(pendingById map[string]*resource) {
if len(pendingById) == 0 {
return
}

type opData struct {
descriptions []string
count int
}
opToOpData := make(map[string]*opData)
count := 0
for id, resource := range pendingById {
description := id
if resource.refs > 1 {
description = fmt.Sprintf("%s (x%d)", description, resource.refs)
}
if _, found := opToOpData[resource.op]; !found {
opToOpData[resource.op] = &opData{}
}
opToOpData[resource.op].descriptions =
append(opToOpData[resource.op].descriptions, description)
opToOpData[resource.op].count += resource.refs
count += resource.refs
}

var descriptions []string
for op, opData := range opToOpData {
// Canonicalize message ordering.
sort.Strings(opData.descriptions)
descriptions = append(descriptions, fmt.Sprintf(
"%s %d (%s)",
op, opData.count, strings.Join(opData.descriptions, ", ")))
}
// Canonicalize message ordering.
sort.Strings(descriptions)

operations := "operations"
if count == 1 {
operations = "operation"
}
h.Ui.Output(h.Colorize.Color(fmt.Sprintf(
"[reset][bold]%d %s pending[reset_bold]: %s",
count, operations,
strings.Join(descriptions, "; "))))
}
15 changes: 5 additions & 10 deletions command/hook_ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ type UiHook struct {
l sync.Mutex
once sync.Once
resources map[string]uiResourceOp
ui cli.Ui
}

type uiResourceOp byte
Expand Down Expand Up @@ -107,7 +106,7 @@ func (h *UiHook) PreApply(
attrString = "\n " + attrString
}

h.ui.Output(h.Colorize.Color(fmt.Sprintf(
h.Ui.Output(h.Colorize.Color(fmt.Sprintf(
"[reset][bold]%s: %s[reset_bold]%s",
id,
operation,
Expand Down Expand Up @@ -144,7 +143,7 @@ func (h *UiHook) PostApply(
return terraform.HookActionContinue, nil
}

h.ui.Output(h.Colorize.Color(fmt.Sprintf(
h.Ui.Output(h.Colorize.Color(fmt.Sprintf(
"[reset][bold]%s: %s[reset_bold]",
id, msg)))

Expand All @@ -161,7 +160,7 @@ func (h *UiHook) PreProvision(
n *terraform.InstanceInfo,
provId string) (terraform.HookAction, error) {
id := n.HumanId()
h.ui.Output(h.Colorize.Color(fmt.Sprintf(
h.Ui.Output(h.Colorize.Color(fmt.Sprintf(
"[reset][bold]%s: Provisioning with '%s'...[reset_bold]",
id, provId)))
return terraform.HookActionContinue, nil
Expand All @@ -185,7 +184,7 @@ func (h *UiHook) ProvisionOutput(
}
}

h.ui.Output(strings.TrimSpace(buf.String()))
h.Ui.Output(strings.TrimSpace(buf.String()))
}

func (h *UiHook) PreRefresh(
Expand All @@ -194,7 +193,7 @@ func (h *UiHook) PreRefresh(
h.once.Do(h.init)

id := n.HumanId()
h.ui.Output(h.Colorize.Color(fmt.Sprintf(
h.Ui.Output(h.Colorize.Color(fmt.Sprintf(
"[reset][bold]%s: Refreshing state... (ID: %s)",
id, s.ID)))
return terraform.HookActionContinue, nil
Expand All @@ -206,10 +205,6 @@ func (h *UiHook) init() {
}

h.resources = make(map[string]uiResourceOp)

// Wrap the ui so that it is safe for concurrency regardless of the
// underlying reader/writer that is in place.
h.ui = &cli.ConcurrentUi{Ui: h.Ui}
}

// scanLines is basically copied from the Go standard library except
Expand Down

0 comments on commit ade9d9e

Please sign in to comment.