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