github.com/brandonstevens/terraform@v0.9.6-0.20170512224929-5367f2607e16/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 defaultPeriodicUiTimer = 10 * time.Second
    19  const maxIdLen = 80
    20  
    21  type UiHook struct {
    22  	terraform.NilHook
    23  
    24  	Colorize        *colorstring.Colorize
    25  	Ui              cli.Ui
    26  	PeriodicUiTimer time.Duration
    27  
    28  	l         sync.Mutex
    29  	once      sync.Once
    30  	resources map[string]uiResourceState
    31  	ui        cli.Ui
    32  }
    33  
    34  // uiResourceState tracks the state of a single resource
    35  type uiResourceState struct {
    36  	Name       string
    37  	ResourceId string
    38  	Op         uiResourceOp
    39  	Start      time.Time
    40  
    41  	DoneCh chan struct{} // To be used for cancellation
    42  
    43  	done chan struct{} // used to coordinate tests
    44  }
    45  
    46  // uiResourceOp is an enum for operations on a resource
    47  type uiResourceOp byte
    48  
    49  const (
    50  	uiResourceUnknown uiResourceOp = iota
    51  	uiResourceCreate
    52  	uiResourceModify
    53  	uiResourceDestroy
    54  )
    55  
    56  func (h *UiHook) PreApply(
    57  	n *terraform.InstanceInfo,
    58  	s *terraform.InstanceState,
    59  	d *terraform.InstanceDiff) (terraform.HookAction, error) {
    60  	h.once.Do(h.init)
    61  
    62  	// if there's no diff, there's nothing to output
    63  	if d.Empty() {
    64  		return terraform.HookActionContinue, nil
    65  	}
    66  
    67  	id := n.HumanId()
    68  
    69  	op := uiResourceModify
    70  	if d.Destroy {
    71  		op = uiResourceDestroy
    72  	} else if s.ID == "" {
    73  		op = uiResourceCreate
    74  	}
    75  
    76  	var operation string
    77  	switch op {
    78  	case uiResourceModify:
    79  		operation = "Modifying..."
    80  	case uiResourceDestroy:
    81  		operation = "Destroying..."
    82  	case uiResourceCreate:
    83  		operation = "Creating..."
    84  	case uiResourceUnknown:
    85  		return terraform.HookActionContinue, nil
    86  	}
    87  
    88  	attrBuf := new(bytes.Buffer)
    89  
    90  	// Get all the attributes that are changing, and sort them. Also
    91  	// determine the longest key so that we can align them all.
    92  	keyLen := 0
    93  
    94  	dAttrs := d.CopyAttributes()
    95  	keys := make([]string, 0, len(dAttrs))
    96  	for key, _ := range dAttrs {
    97  		// Skip the ID since we do that specially
    98  		if key == "id" {
    99  			continue
   100  		}
   101  
   102  		keys = append(keys, key)
   103  		if len(key) > keyLen {
   104  			keyLen = len(key)
   105  		}
   106  	}
   107  	sort.Strings(keys)
   108  
   109  	// Go through and output each attribute
   110  	for _, attrK := range keys {
   111  		attrDiff, _ := d.GetAttribute(attrK)
   112  
   113  		v := attrDiff.New
   114  		u := attrDiff.Old
   115  		if attrDiff.NewComputed {
   116  			v = "<computed>"
   117  		}
   118  
   119  		if attrDiff.Sensitive {
   120  			u = "<sensitive>"
   121  			v = "<sensitive>"
   122  		}
   123  
   124  		attrBuf.WriteString(fmt.Sprintf(
   125  			"  %s:%s %#v => %#v\n",
   126  			attrK,
   127  			strings.Repeat(" ", keyLen-len(attrK)),
   128  			u,
   129  			v))
   130  	}
   131  
   132  	attrString := strings.TrimSpace(attrBuf.String())
   133  	if attrString != "" {
   134  		attrString = "\n  " + attrString
   135  	}
   136  
   137  	var stateId, stateIdSuffix string
   138  	if s != nil && s.ID != "" {
   139  		stateId = s.ID
   140  		stateIdSuffix = fmt.Sprintf(" (ID: %s)", truncateId(s.ID, maxIdLen))
   141  	}
   142  
   143  	h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   144  		"[reset][bold]%s: %s%s[reset]%s",
   145  		id,
   146  		operation,
   147  		stateIdSuffix,
   148  		attrString)))
   149  
   150  	uiState := uiResourceState{
   151  		Name:       id,
   152  		ResourceId: stateId,
   153  		Op:         op,
   154  		Start:      time.Now().Round(time.Second),
   155  		DoneCh:     make(chan struct{}),
   156  		done:       make(chan struct{}),
   157  	}
   158  
   159  	h.l.Lock()
   160  	h.resources[id] = uiState
   161  	h.l.Unlock()
   162  
   163  	// Start goroutine that shows progress
   164  	go h.stillApplying(uiState)
   165  
   166  	return terraform.HookActionContinue, nil
   167  }
   168  
   169  func (h *UiHook) stillApplying(state uiResourceState) {
   170  	defer close(state.done)
   171  	for {
   172  		select {
   173  		case <-state.DoneCh:
   174  			return
   175  
   176  		case <-time.After(h.PeriodicUiTimer):
   177  			// Timer up, show status
   178  		}
   179  
   180  		var msg string
   181  		switch state.Op {
   182  		case uiResourceModify:
   183  			msg = "Still modifying..."
   184  		case uiResourceDestroy:
   185  			msg = "Still destroying..."
   186  		case uiResourceCreate:
   187  			msg = "Still creating..."
   188  		case uiResourceUnknown:
   189  			return
   190  		}
   191  
   192  		idSuffix := ""
   193  		if v := state.ResourceId; v != "" {
   194  			idSuffix = fmt.Sprintf("ID: %s, ", truncateId(v, maxIdLen))
   195  		}
   196  
   197  		h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   198  			"[reset][bold]%s: %s (%s%s elapsed)[reset]",
   199  			state.Name,
   200  			msg,
   201  			idSuffix,
   202  			time.Now().Round(time.Second).Sub(state.Start),
   203  		)))
   204  	}
   205  }
   206  
   207  func (h *UiHook) PostApply(
   208  	n *terraform.InstanceInfo,
   209  	s *terraform.InstanceState,
   210  	applyerr error) (terraform.HookAction, error) {
   211  
   212  	id := n.HumanId()
   213  
   214  	h.l.Lock()
   215  	state := h.resources[id]
   216  	if state.DoneCh != nil {
   217  		close(state.DoneCh)
   218  	}
   219  
   220  	delete(h.resources, id)
   221  	h.l.Unlock()
   222  
   223  	var stateIdSuffix string
   224  	if s != nil && s.ID != "" {
   225  		stateIdSuffix = fmt.Sprintf(" (ID: %s)", truncateId(s.ID, maxIdLen))
   226  	}
   227  
   228  	var msg string
   229  	switch state.Op {
   230  	case uiResourceModify:
   231  		msg = "Modifications complete"
   232  	case uiResourceDestroy:
   233  		msg = "Destruction complete"
   234  	case uiResourceCreate:
   235  		msg = "Creation complete"
   236  	case uiResourceUnknown:
   237  		return terraform.HookActionContinue, nil
   238  	}
   239  
   240  	if applyerr != nil {
   241  		// Errors are collected and printed in ApplyCommand, no need to duplicate
   242  		return terraform.HookActionContinue, nil
   243  	}
   244  
   245  	colorized := h.Colorize.Color(fmt.Sprintf(
   246  		"[reset][bold]%s: %s%s[reset]",
   247  		id, msg, stateIdSuffix))
   248  
   249  	h.ui.Output(colorized)
   250  
   251  	return terraform.HookActionContinue, nil
   252  }
   253  
   254  func (h *UiHook) PreDiff(
   255  	n *terraform.InstanceInfo,
   256  	s *terraform.InstanceState) (terraform.HookAction, error) {
   257  	return terraform.HookActionContinue, nil
   258  }
   259  
   260  func (h *UiHook) PreProvision(
   261  	n *terraform.InstanceInfo,
   262  	provId string) (terraform.HookAction, error) {
   263  	id := n.HumanId()
   264  	h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   265  		"[reset][bold]%s: Provisioning with '%s'...[reset]",
   266  		id, provId)))
   267  	return terraform.HookActionContinue, nil
   268  }
   269  
   270  func (h *UiHook) ProvisionOutput(
   271  	n *terraform.InstanceInfo,
   272  	provId string,
   273  	msg string) {
   274  	id := n.HumanId()
   275  	var buf bytes.Buffer
   276  	buf.WriteString(h.Colorize.Color("[reset]"))
   277  
   278  	prefix := fmt.Sprintf("%s (%s): ", id, provId)
   279  	s := bufio.NewScanner(strings.NewReader(msg))
   280  	s.Split(scanLines)
   281  	for s.Scan() {
   282  		line := strings.TrimRightFunc(s.Text(), unicode.IsSpace)
   283  		if line != "" {
   284  			buf.WriteString(fmt.Sprintf("%s%s\n", prefix, line))
   285  		}
   286  	}
   287  
   288  	h.ui.Output(strings.TrimSpace(buf.String()))
   289  }
   290  
   291  func (h *UiHook) PreRefresh(
   292  	n *terraform.InstanceInfo,
   293  	s *terraform.InstanceState) (terraform.HookAction, error) {
   294  	h.once.Do(h.init)
   295  
   296  	id := n.HumanId()
   297  
   298  	var stateIdSuffix string
   299  	// Data resources refresh before they have ids, whereas managed
   300  	// resources are only refreshed when they have ids.
   301  	if s.ID != "" {
   302  		stateIdSuffix = fmt.Sprintf(" (ID: %s)", truncateId(s.ID, maxIdLen))
   303  	}
   304  
   305  	h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   306  		"[reset][bold]%s: Refreshing state...%s",
   307  		id, stateIdSuffix)))
   308  	return terraform.HookActionContinue, nil
   309  }
   310  
   311  func (h *UiHook) PreImportState(
   312  	n *terraform.InstanceInfo,
   313  	id string) (terraform.HookAction, error) {
   314  	h.once.Do(h.init)
   315  
   316  	h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   317  		"[reset][bold]%s: Importing from ID %q...",
   318  		n.HumanId(), id)))
   319  	return terraform.HookActionContinue, nil
   320  }
   321  
   322  func (h *UiHook) PostImportState(
   323  	n *terraform.InstanceInfo,
   324  	s []*terraform.InstanceState) (terraform.HookAction, error) {
   325  	h.once.Do(h.init)
   326  
   327  	id := n.HumanId()
   328  	h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   329  		"[reset][bold][green]%s: Import complete!", id)))
   330  	for _, s := range s {
   331  		h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   332  			"[reset][green]  Imported %s (ID: %s)",
   333  			s.Ephemeral.Type, s.ID)))
   334  	}
   335  
   336  	return terraform.HookActionContinue, nil
   337  }
   338  
   339  func (h *UiHook) init() {
   340  	if h.Colorize == nil {
   341  		panic("colorize not given")
   342  	}
   343  	if h.PeriodicUiTimer == 0 {
   344  		h.PeriodicUiTimer = defaultPeriodicUiTimer
   345  	}
   346  
   347  	h.resources = make(map[string]uiResourceState)
   348  
   349  	// Wrap the ui so that it is safe for concurrency regardless of the
   350  	// underlying reader/writer that is in place.
   351  	h.ui = &cli.ConcurrentUi{Ui: h.Ui}
   352  }
   353  
   354  // scanLines is basically copied from the Go standard library except
   355  // we've modified it to also fine `\r`.
   356  func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
   357  	if atEOF && len(data) == 0 {
   358  		return 0, nil, nil
   359  	}
   360  	if i := bytes.IndexByte(data, '\n'); i >= 0 {
   361  		// We have a full newline-terminated line.
   362  		return i + 1, dropCR(data[0:i]), nil
   363  	}
   364  	if i := bytes.IndexByte(data, '\r'); i >= 0 {
   365  		// We have a full newline-terminated line.
   366  		return i + 1, dropCR(data[0:i]), nil
   367  	}
   368  	// If we're at EOF, we have a final, non-terminated line. Return it.
   369  	if atEOF {
   370  		return len(data), dropCR(data), nil
   371  	}
   372  	// Request more data.
   373  	return 0, nil, nil
   374  }
   375  
   376  // dropCR drops a terminal \r from the data.
   377  func dropCR(data []byte) []byte {
   378  	if len(data) > 0 && data[len(data)-1] == '\r' {
   379  		return data[0 : len(data)-1]
   380  	}
   381  	return data
   382  }
   383  
   384  func truncateId(id string, maxLen int) string {
   385  	totalLength := len(id)
   386  	if totalLength <= maxLen {
   387  		return id
   388  	}
   389  	if maxLen < 5 {
   390  		// We don't shorten to less than 5 chars
   391  		// as that would be pointless with ... (3 chars)
   392  		maxLen = 5
   393  	}
   394  
   395  	dots := "..."
   396  	partLen := maxLen / 2
   397  
   398  	leftIdx := partLen - 1
   399  	leftPart := id[0:leftIdx]
   400  
   401  	rightIdx := totalLength - partLen - 1
   402  
   403  	overlap := maxLen - (partLen*2 + len(dots))
   404  	if overlap < 0 {
   405  		rightIdx -= overlap
   406  	}
   407  
   408  	rightPart := id[rightIdx:]
   409  
   410  	return leftPart + dots + rightPart
   411  }