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