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