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