github.com/adrian-bl/terraform@v0.7.0-rc2.0.20160705220747-de0a34fc3517/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  		u := attrDiff.Old
   107  		if attrDiff.NewComputed {
   108  			v = "<computed>"
   109  		}
   110  
   111  		if attrDiff.Sensitive {
   112  			u = "<sensitive>"
   113  			v = "<sensitive>"
   114  		}
   115  
   116  		attrBuf.WriteString(fmt.Sprintf(
   117  			"  %s:%s %#v => %#v\n",
   118  			attrK,
   119  			strings.Repeat(" ", keyLen-len(attrK)),
   120  			u,
   121  			v))
   122  	}
   123  
   124  	attrString := strings.TrimSpace(attrBuf.String())
   125  	if attrString != "" {
   126  		attrString = "\n  " + attrString
   127  	}
   128  
   129  	h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   130  		"[reset][bold]%s: %s[reset_bold]%s",
   131  		id,
   132  		operation,
   133  		attrString)))
   134  
   135  	// Set a timer to show an operation is still happening
   136  	time.AfterFunc(periodicUiTimer, func() { h.stillApplying(id) })
   137  
   138  	return terraform.HookActionContinue, nil
   139  }
   140  
   141  func (h *UiHook) stillApplying(id string) {
   142  	// Grab the operation. We defer the lock here to avoid the "still..."
   143  	// message showing up after a completion message.
   144  	h.l.Lock()
   145  	defer h.l.Unlock()
   146  	state, ok := h.resources[id]
   147  
   148  	// If the resource is out of the map it means we're done with it
   149  	if !ok {
   150  		return
   151  	}
   152  
   153  	var msg string
   154  	switch state.Op {
   155  	case uiResourceModify:
   156  		msg = "Still modifying..."
   157  	case uiResourceDestroy:
   158  		msg = "Still destroying..."
   159  	case uiResourceCreate:
   160  		msg = "Still creating..."
   161  	case uiResourceUnknown:
   162  		return
   163  	}
   164  
   165  	h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   166  		"[reset][bold]%s: %s (%s elapsed)[reset_bold]",
   167  		id,
   168  		msg,
   169  		time.Now().Round(time.Second).Sub(state.Start),
   170  	)))
   171  
   172  	// Reschedule
   173  	time.AfterFunc(periodicUiTimer, func() { h.stillApplying(id) })
   174  }
   175  
   176  func (h *UiHook) PostApply(
   177  	n *terraform.InstanceInfo,
   178  	s *terraform.InstanceState,
   179  	applyerr error) (terraform.HookAction, error) {
   180  	id := n.HumanId()
   181  
   182  	h.l.Lock()
   183  	state := h.resources[id]
   184  	delete(h.resources, id)
   185  	h.l.Unlock()
   186  
   187  	var msg string
   188  	switch state.Op {
   189  	case uiResourceModify:
   190  		msg = "Modifications complete"
   191  	case uiResourceDestroy:
   192  		msg = "Destruction complete"
   193  	case uiResourceCreate:
   194  		msg = "Creation complete"
   195  	case uiResourceUnknown:
   196  		return terraform.HookActionContinue, nil
   197  	}
   198  
   199  	if applyerr != nil {
   200  		// Errors are collected and printed in ApplyCommand, no need to duplicate
   201  		return terraform.HookActionContinue, nil
   202  	}
   203  
   204  	h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   205  		"[reset][bold]%s: %s[reset_bold]",
   206  		id, msg)))
   207  
   208  	return terraform.HookActionContinue, nil
   209  }
   210  
   211  func (h *UiHook) PreDiff(
   212  	n *terraform.InstanceInfo,
   213  	s *terraform.InstanceState) (terraform.HookAction, error) {
   214  	return terraform.HookActionContinue, nil
   215  }
   216  
   217  func (h *UiHook) PreProvision(
   218  	n *terraform.InstanceInfo,
   219  	provId string) (terraform.HookAction, error) {
   220  	id := n.HumanId()
   221  	h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   222  		"[reset][bold]%s: Provisioning with '%s'...[reset_bold]",
   223  		id, provId)))
   224  	return terraform.HookActionContinue, nil
   225  }
   226  
   227  func (h *UiHook) ProvisionOutput(
   228  	n *terraform.InstanceInfo,
   229  	provId string,
   230  	msg string) {
   231  	id := n.HumanId()
   232  	var buf bytes.Buffer
   233  	buf.WriteString(h.Colorize.Color("[reset]"))
   234  
   235  	prefix := fmt.Sprintf("%s (%s): ", id, provId)
   236  	s := bufio.NewScanner(strings.NewReader(msg))
   237  	s.Split(scanLines)
   238  	for s.Scan() {
   239  		line := strings.TrimRightFunc(s.Text(), unicode.IsSpace)
   240  		if line != "" {
   241  			buf.WriteString(fmt.Sprintf("%s%s\n", prefix, line))
   242  		}
   243  	}
   244  
   245  	h.ui.Output(strings.TrimSpace(buf.String()))
   246  }
   247  
   248  func (h *UiHook) PreRefresh(
   249  	n *terraform.InstanceInfo,
   250  	s *terraform.InstanceState) (terraform.HookAction, error) {
   251  	h.once.Do(h.init)
   252  
   253  	id := n.HumanId()
   254  
   255  	var stateIdSuffix string
   256  	// Data resources refresh before they have ids, whereas managed
   257  	// resources are only refreshed when they have ids.
   258  	if s.ID != "" {
   259  		stateIdSuffix = fmt.Sprintf(" (ID: %s)", s.ID)
   260  	}
   261  
   262  	h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   263  		"[reset][bold]%s: Refreshing state...%s",
   264  		id, stateIdSuffix)))
   265  	return terraform.HookActionContinue, nil
   266  }
   267  
   268  func (h *UiHook) PreImportState(
   269  	n *terraform.InstanceInfo,
   270  	id string) (terraform.HookAction, error) {
   271  	h.once.Do(h.init)
   272  
   273  	h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   274  		"[reset][bold]%s: Importing from ID %q...",
   275  		n.HumanId(), id)))
   276  	return terraform.HookActionContinue, nil
   277  }
   278  
   279  func (h *UiHook) PostImportState(
   280  	n *terraform.InstanceInfo,
   281  	s []*terraform.InstanceState) (terraform.HookAction, error) {
   282  	h.once.Do(h.init)
   283  
   284  	id := n.HumanId()
   285  	h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   286  		"[reset][bold][green]%s: Import complete!", id)))
   287  	for _, s := range s {
   288  		h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   289  			"[reset][green]  Imported %s (ID: %s)",
   290  			s.Ephemeral.Type, s.ID)))
   291  	}
   292  
   293  	return terraform.HookActionContinue, nil
   294  }
   295  
   296  func (h *UiHook) init() {
   297  	if h.Colorize == nil {
   298  		panic("colorize not given")
   299  	}
   300  
   301  	h.resources = make(map[string]uiResourceState)
   302  
   303  	// Wrap the ui so that it is safe for concurrency regardless of the
   304  	// underlying reader/writer that is in place.
   305  	h.ui = &cli.ConcurrentUi{Ui: h.Ui}
   306  }
   307  
   308  // scanLines is basically copied from the Go standard library except
   309  // we've modified it to also fine `\r`.
   310  func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
   311  	if atEOF && len(data) == 0 {
   312  		return 0, nil, nil
   313  	}
   314  	if i := bytes.IndexByte(data, '\n'); i >= 0 {
   315  		// We have a full newline-terminated line.
   316  		return i + 1, dropCR(data[0:i]), nil
   317  	}
   318  	if i := bytes.IndexByte(data, '\r'); i >= 0 {
   319  		// We have a full newline-terminated line.
   320  		return i + 1, dropCR(data[0:i]), nil
   321  	}
   322  	// If we're at EOF, we have a final, non-terminated line. Return it.
   323  	if atEOF {
   324  		return len(data), dropCR(data), nil
   325  	}
   326  	// Request more data.
   327  	return 0, nil, nil
   328  }
   329  
   330  // dropCR drops a terminal \r from the data.
   331  func dropCR(data []byte) []byte {
   332  	if len(data) > 0 && data[len(data)-1] == '\r' {
   333  		return data[0 : len(data)-1]
   334  	}
   335  	return data
   336  }