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