github.com/sarguru/terraform@v0.6.17-0.20160525232901-8fcdfd7e3dc9/command/hook_ui.go (about)

     1  package command
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"sort"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  	"unicode"
    12  
    13  	"github.com/hashicorp/terraform/terraform"
    14  	"github.com/mitchellh/cli"
    15  	"github.com/mitchellh/colorstring"
    16  )
    17  
    18  const periodicUiTimer = 10 * time.Second
    19  
    20  type UiHook struct {
    21  	terraform.NilHook
    22  
    23  	Colorize *colorstring.Colorize
    24  	Ui       cli.Ui
    25  
    26  	l         sync.Mutex
    27  	once      sync.Once
    28  	resources map[string]uiResourceState
    29  	ui        cli.Ui
    30  }
    31  
    32  // uiResourceState tracks the state of a single resource
    33  type uiResourceState struct {
    34  	Op    uiResourceOp
    35  	Start time.Time
    36  }
    37  
    38  // uiResourceOp is an enum for operations on a resource
    39  type uiResourceOp byte
    40  
    41  const (
    42  	uiResourceUnknown uiResourceOp = iota
    43  	uiResourceCreate
    44  	uiResourceModify
    45  	uiResourceDestroy
    46  )
    47  
    48  func (h *UiHook) PreApply(
    49  	n *terraform.InstanceInfo,
    50  	s *terraform.InstanceState,
    51  	d *terraform.InstanceDiff) (terraform.HookAction, error) {
    52  	h.once.Do(h.init)
    53  
    54  	id := n.HumanId()
    55  
    56  	op := uiResourceModify
    57  	if d.Destroy {
    58  		op = uiResourceDestroy
    59  	} else if s.ID == "" {
    60  		op = uiResourceCreate
    61  	}
    62  
    63  	h.l.Lock()
    64  	h.resources[id] = uiResourceState{
    65  		Op:    op,
    66  		Start: time.Now().Round(time.Second),
    67  	}
    68  	h.l.Unlock()
    69  
    70  	var operation string
    71  	switch op {
    72  	case uiResourceModify:
    73  		operation = "Modifying..."
    74  	case uiResourceDestroy:
    75  		operation = "Destroying..."
    76  	case uiResourceCreate:
    77  		operation = "Creating..."
    78  	case uiResourceUnknown:
    79  		return terraform.HookActionContinue, nil
    80  	}
    81  
    82  	attrBuf := new(bytes.Buffer)
    83  
    84  	// Get all the attributes that are changing, and sort them. Also
    85  	// determine the longest key so that we can align them all.
    86  	keyLen := 0
    87  	keys := make([]string, 0, len(d.Attributes))
    88  	for key, _ := range d.Attributes {
    89  		// Skip the ID since we do that specially
    90  		if key == "id" {
    91  			continue
    92  		}
    93  
    94  		keys = append(keys, key)
    95  		if len(key) > keyLen {
    96  			keyLen = len(key)
    97  		}
    98  	}
    99  	sort.Strings(keys)
   100  
   101  	// Go through and output each attribute
   102  	for _, attrK := range keys {
   103  		attrDiff := d.Attributes[attrK]
   104  
   105  		v := attrDiff.New
   106  		if attrDiff.NewComputed {
   107  			v = "<computed>"
   108  		}
   109  
   110  		attrBuf.WriteString(fmt.Sprintf(
   111  			"  %s:%s %#v => %#v\n",
   112  			attrK,
   113  			strings.Repeat(" ", keyLen-len(attrK)),
   114  			attrDiff.Old,
   115  			v))
   116  	}
   117  
   118  	attrString := strings.TrimSpace(attrBuf.String())
   119  	if attrString != "" {
   120  		attrString = "\n  " + attrString
   121  	}
   122  
   123  	h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   124  		"[reset][bold]%s: %s[reset_bold]%s",
   125  		id,
   126  		operation,
   127  		attrString)))
   128  
   129  	// Set a timer to show an operation is still happening
   130  	time.AfterFunc(periodicUiTimer, func() { h.stillApplying(id) })
   131  
   132  	return terraform.HookActionContinue, nil
   133  }
   134  
   135  func (h *UiHook) stillApplying(id string) {
   136  	// Grab the operation. We defer the lock here to avoid the "still..."
   137  	// message showing up after a completion message.
   138  	h.l.Lock()
   139  	defer h.l.Unlock()
   140  	state, ok := h.resources[id]
   141  
   142  	// If the resource is out of the map it means we're done with it
   143  	if !ok {
   144  		return
   145  	}
   146  
   147  	var msg string
   148  	switch state.Op {
   149  	case uiResourceModify:
   150  		msg = "Still modifying..."
   151  	case uiResourceDestroy:
   152  		msg = "Still destroying..."
   153  	case uiResourceCreate:
   154  		msg = "Still creating..."
   155  	case uiResourceUnknown:
   156  		return
   157  	}
   158  
   159  	h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   160  		"[reset][bold]%s: %s (%s elapsed)[reset_bold]",
   161  		id,
   162  		msg,
   163  		time.Now().Round(time.Second).Sub(state.Start),
   164  	)))
   165  
   166  	// Reschedule
   167  	time.AfterFunc(periodicUiTimer, func() { h.stillApplying(id) })
   168  }
   169  
   170  func (h *UiHook) PostApply(
   171  	n *terraform.InstanceInfo,
   172  	s *terraform.InstanceState,
   173  	applyerr error) (terraform.HookAction, error) {
   174  	id := n.HumanId()
   175  
   176  	h.l.Lock()
   177  	state := h.resources[id]
   178  	delete(h.resources, id)
   179  	h.l.Unlock()
   180  
   181  	var msg string
   182  	switch state.Op {
   183  	case uiResourceModify:
   184  		msg = "Modifications complete"
   185  	case uiResourceDestroy:
   186  		msg = "Destruction complete"
   187  	case uiResourceCreate:
   188  		msg = "Creation complete"
   189  	case uiResourceUnknown:
   190  		return terraform.HookActionContinue, nil
   191  	}
   192  
   193  	if applyerr != nil {
   194  		// Errors are collected and printed in ApplyCommand, no need to duplicate
   195  		return terraform.HookActionContinue, nil
   196  	}
   197  
   198  	h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   199  		"[reset][bold]%s: %s[reset_bold]",
   200  		id, msg)))
   201  
   202  	return terraform.HookActionContinue, nil
   203  }
   204  
   205  func (h *UiHook) PreDiff(
   206  	n *terraform.InstanceInfo,
   207  	s *terraform.InstanceState) (terraform.HookAction, error) {
   208  	return terraform.HookActionContinue, nil
   209  }
   210  
   211  func (h *UiHook) PreProvision(
   212  	n *terraform.InstanceInfo,
   213  	provId string) (terraform.HookAction, error) {
   214  	id := n.HumanId()
   215  	h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   216  		"[reset][bold]%s: Provisioning with '%s'...[reset_bold]",
   217  		id, provId)))
   218  	return terraform.HookActionContinue, nil
   219  }
   220  
   221  func (h *UiHook) ProvisionOutput(
   222  	n *terraform.InstanceInfo,
   223  	provId string,
   224  	msg string) {
   225  	id := n.HumanId()
   226  	var buf bytes.Buffer
   227  	buf.WriteString(h.Colorize.Color("[reset]"))
   228  
   229  	prefix := fmt.Sprintf("%s (%s): ", id, provId)
   230  	s := bufio.NewScanner(strings.NewReader(msg))
   231  	s.Split(scanLines)
   232  	for s.Scan() {
   233  		line := strings.TrimRightFunc(s.Text(), unicode.IsSpace)
   234  		if line != "" {
   235  			buf.WriteString(fmt.Sprintf("%s%s\n", prefix, line))
   236  		}
   237  	}
   238  
   239  	h.ui.Output(strings.TrimSpace(buf.String()))
   240  }
   241  
   242  func (h *UiHook) PreRefresh(
   243  	n *terraform.InstanceInfo,
   244  	s *terraform.InstanceState) (terraform.HookAction, error) {
   245  	h.once.Do(h.init)
   246  
   247  	id := n.HumanId()
   248  
   249  	var stateIdSuffix string
   250  	// Data resources refresh before they have ids, whereas managed
   251  	// resources are only refreshed when they have ids.
   252  	if s.ID != "" {
   253  		stateIdSuffix = fmt.Sprintf(" (ID: %s)", s.ID)
   254  	}
   255  
   256  	h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   257  		"[reset][bold]%s: Refreshing state...%s",
   258  		id, stateIdSuffix)))
   259  	return terraform.HookActionContinue, nil
   260  }
   261  
   262  func (h *UiHook) PreImportState(
   263  	n *terraform.InstanceInfo,
   264  	id string) (terraform.HookAction, error) {
   265  	h.once.Do(h.init)
   266  
   267  	h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   268  		"[reset][bold]%s: Importing from ID %q...",
   269  		n.HumanId(), id)))
   270  	return terraform.HookActionContinue, nil
   271  }
   272  
   273  func (h *UiHook) PostImportState(
   274  	n *terraform.InstanceInfo,
   275  	s []*terraform.InstanceState) (terraform.HookAction, error) {
   276  	h.once.Do(h.init)
   277  
   278  	id := n.HumanId()
   279  	h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   280  		"[reset][bold][green]%s: Import complete!", id)))
   281  	for _, s := range s {
   282  		h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   283  			"[reset][green]  Imported %s (ID: %s)",
   284  			s.Ephemeral.Type, s.ID)))
   285  	}
   286  
   287  	return terraform.HookActionContinue, nil
   288  }
   289  
   290  func (h *UiHook) init() {
   291  	if h.Colorize == nil {
   292  		panic("colorize not given")
   293  	}
   294  
   295  	h.resources = make(map[string]uiResourceState)
   296  
   297  	// Wrap the ui so that it is safe for concurrency regardless of the
   298  	// underlying reader/writer that is in place.
   299  	h.ui = &cli.ConcurrentUi{Ui: h.Ui}
   300  }
   301  
   302  // scanLines is basically copied from the Go standard library except
   303  // we've modified it to also fine `\r`.
   304  func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
   305  	if atEOF && len(data) == 0 {
   306  		return 0, nil, nil
   307  	}
   308  	if i := bytes.IndexByte(data, '\n'); i >= 0 {
   309  		// We have a full newline-terminated line.
   310  		return i + 1, dropCR(data[0:i]), nil
   311  	}
   312  	if i := bytes.IndexByte(data, '\r'); i >= 0 {
   313  		// We have a full newline-terminated line.
   314  		return i + 1, dropCR(data[0:i]), nil
   315  	}
   316  	// If we're at EOF, we have a final, non-terminated line. Return it.
   317  	if atEOF {
   318  		return len(data), dropCR(data), nil
   319  	}
   320  	// Request more data.
   321  	return 0, nil, nil
   322  }
   323  
   324  // dropCR drops a terminal \r from the data.
   325  func dropCR(data []byte) []byte {
   326  	if len(data) > 0 && data[len(data)-1] == '\r' {
   327  		return data[0 : len(data)-1]
   328  	}
   329  	return data
   330  }