github.com/hugorut/terraform@v1.1.3/src/command/views/hook_ui.go (about)

     1  package views
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"strings"
     8  	"sync"
     9  	"time"
    10  	"unicode"
    11  
    12  	"github.com/zclconf/go-cty/cty"
    13  
    14  	"github.com/hugorut/terraform/src/addrs"
    15  	"github.com/hugorut/terraform/src/command/format"
    16  	"github.com/hugorut/terraform/src/plans"
    17  	"github.com/hugorut/terraform/src/providers"
    18  	"github.com/hugorut/terraform/src/states"
    19  	"github.com/hugorut/terraform/src/terraform"
    20  )
    21  
    22  const defaultPeriodicUiTimer = 10 * time.Second
    23  const maxIdLen = 80
    24  
    25  func NewUiHook(view *View) *UiHook {
    26  	return &UiHook{
    27  		view:            view,
    28  		periodicUiTimer: defaultPeriodicUiTimer,
    29  		resources:       make(map[string]uiResourceState),
    30  	}
    31  }
    32  
    33  type UiHook struct {
    34  	terraform.NilHook
    35  
    36  	view     *View
    37  	viewLock sync.Mutex
    38  
    39  	periodicUiTimer time.Duration
    40  
    41  	resources     map[string]uiResourceState
    42  	resourcesLock sync.Mutex
    43  }
    44  
    45  var _ terraform.Hook = (*UiHook)(nil)
    46  
    47  // uiResourceState tracks the state of a single resource
    48  type uiResourceState struct {
    49  	DispAddr       string
    50  	IDKey, IDValue string
    51  	Op             uiResourceOp
    52  	Start          time.Time
    53  
    54  	DoneCh chan struct{} // To be used for cancellation
    55  
    56  	done chan struct{} // used to coordinate tests
    57  }
    58  
    59  // uiResourceOp is an enum for operations on a resource
    60  type uiResourceOp byte
    61  
    62  const (
    63  	uiResourceUnknown uiResourceOp = iota
    64  	uiResourceCreate
    65  	uiResourceModify
    66  	uiResourceDestroy
    67  	uiResourceRead
    68  )
    69  
    70  func (h *UiHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) {
    71  	dispAddr := addr.String()
    72  	if gen != states.CurrentGen {
    73  		dispAddr = fmt.Sprintf("%s (deposed object %s)", dispAddr, gen)
    74  	}
    75  
    76  	var operation string
    77  	var op uiResourceOp
    78  	idKey, idValue := format.ObjectValueIDOrName(priorState)
    79  	switch action {
    80  	case plans.Delete:
    81  		operation = "Destroying..."
    82  		op = uiResourceDestroy
    83  	case plans.Create:
    84  		operation = "Creating..."
    85  		op = uiResourceCreate
    86  	case plans.Update:
    87  		operation = "Modifying..."
    88  		op = uiResourceModify
    89  	case plans.Read:
    90  		operation = "Reading..."
    91  		op = uiResourceRead
    92  	default:
    93  		// We don't expect any other actions in here, so anything else is a
    94  		// bug in the caller but we'll ignore it in order to be robust.
    95  		h.println(fmt.Sprintf("(Unknown action %s for %s)", action, dispAddr))
    96  		return terraform.HookActionContinue, nil
    97  	}
    98  
    99  	var stateIdSuffix string
   100  	if idKey != "" && idValue != "" {
   101  		stateIdSuffix = fmt.Sprintf(" [%s=%s]", idKey, idValue)
   102  	} else {
   103  		// Make sure they are both empty so we can deal with this more
   104  		// easily in the other hook methods.
   105  		idKey = ""
   106  		idValue = ""
   107  	}
   108  
   109  	h.println(fmt.Sprintf(
   110  		h.view.colorize.Color("[reset][bold]%s: %s%s[reset]"),
   111  		dispAddr,
   112  		operation,
   113  		stateIdSuffix,
   114  	))
   115  
   116  	key := addr.String()
   117  	uiState := uiResourceState{
   118  		DispAddr: key,
   119  		IDKey:    idKey,
   120  		IDValue:  idValue,
   121  		Op:       op,
   122  		Start:    time.Now().Round(time.Second),
   123  		DoneCh:   make(chan struct{}),
   124  		done:     make(chan struct{}),
   125  	}
   126  
   127  	h.resourcesLock.Lock()
   128  	h.resources[key] = uiState
   129  	h.resourcesLock.Unlock()
   130  
   131  	// Start goroutine that shows progress
   132  	go h.stillApplying(uiState)
   133  
   134  	return terraform.HookActionContinue, nil
   135  }
   136  
   137  func (h *UiHook) stillApplying(state uiResourceState) {
   138  	defer close(state.done)
   139  	for {
   140  		select {
   141  		case <-state.DoneCh:
   142  			return
   143  
   144  		case <-time.After(h.periodicUiTimer):
   145  			// Timer up, show status
   146  		}
   147  
   148  		var msg string
   149  		switch state.Op {
   150  		case uiResourceModify:
   151  			msg = "Still modifying..."
   152  		case uiResourceDestroy:
   153  			msg = "Still destroying..."
   154  		case uiResourceCreate:
   155  			msg = "Still creating..."
   156  		case uiResourceRead:
   157  			msg = "Still reading..."
   158  		case uiResourceUnknown:
   159  			return
   160  		}
   161  
   162  		idSuffix := ""
   163  		if state.IDKey != "" {
   164  			idSuffix = fmt.Sprintf("%s=%s, ", state.IDKey, truncateId(state.IDValue, maxIdLen))
   165  		}
   166  
   167  		h.println(fmt.Sprintf(
   168  			h.view.colorize.Color("[reset][bold]%s: %s [%s%s elapsed][reset]"),
   169  			state.DispAddr,
   170  			msg,
   171  			idSuffix,
   172  			time.Now().Round(time.Second).Sub(state.Start),
   173  		))
   174  	}
   175  }
   176  
   177  func (h *UiHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, applyerr error) (terraform.HookAction, error) {
   178  	id := addr.String()
   179  
   180  	h.resourcesLock.Lock()
   181  	state := h.resources[id]
   182  	if state.DoneCh != nil {
   183  		close(state.DoneCh)
   184  	}
   185  
   186  	delete(h.resources, id)
   187  	h.resourcesLock.Unlock()
   188  
   189  	var stateIdSuffix string
   190  	if k, v := format.ObjectValueID(newState); k != "" && v != "" {
   191  		stateIdSuffix = fmt.Sprintf(" [%s=%s]", k, v)
   192  	}
   193  
   194  	var msg string
   195  	switch state.Op {
   196  	case uiResourceModify:
   197  		msg = "Modifications complete"
   198  	case uiResourceDestroy:
   199  		msg = "Destruction complete"
   200  	case uiResourceCreate:
   201  		msg = "Creation complete"
   202  	case uiResourceRead:
   203  		msg = "Read complete"
   204  	case uiResourceUnknown:
   205  		return terraform.HookActionContinue, nil
   206  	}
   207  
   208  	if applyerr != nil {
   209  		// Errors are collected and printed in ApplyCommand, no need to duplicate
   210  		return terraform.HookActionContinue, nil
   211  	}
   212  
   213  	addrStr := addr.String()
   214  	if depKey, ok := gen.(states.DeposedKey); ok {
   215  		addrStr = fmt.Sprintf("%s (deposed object %s)", addrStr, depKey)
   216  	}
   217  
   218  	colorized := fmt.Sprintf(
   219  		h.view.colorize.Color("[reset][bold]%s: %s after %s%s"),
   220  		addrStr, msg, time.Now().Round(time.Second).Sub(state.Start), stateIdSuffix)
   221  
   222  	h.println(colorized)
   223  
   224  	return terraform.HookActionContinue, nil
   225  }
   226  
   227  func (h *UiHook) PreProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string) (terraform.HookAction, error) {
   228  	h.println(fmt.Sprintf(
   229  		h.view.colorize.Color("[reset][bold]%s: Provisioning with '%s'...[reset]"),
   230  		addr, typeName,
   231  	))
   232  	return terraform.HookActionContinue, nil
   233  }
   234  
   235  func (h *UiHook) ProvisionOutput(addr addrs.AbsResourceInstance, typeName string, msg string) {
   236  	var buf bytes.Buffer
   237  
   238  	prefix := fmt.Sprintf(
   239  		h.view.colorize.Color("[reset][bold]%s (%s):[reset] "),
   240  		addr, typeName,
   241  	)
   242  	s := bufio.NewScanner(strings.NewReader(msg))
   243  	s.Split(scanLines)
   244  	for s.Scan() {
   245  		line := strings.TrimRightFunc(s.Text(), unicode.IsSpace)
   246  		if line != "" {
   247  			buf.WriteString(fmt.Sprintf("%s%s\n", prefix, line))
   248  		}
   249  	}
   250  
   251  	h.println(strings.TrimSpace(buf.String()))
   252  }
   253  
   254  func (h *UiHook) PreRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value) (terraform.HookAction, error) {
   255  	var stateIdSuffix string
   256  	if k, v := format.ObjectValueID(priorState); k != "" && v != "" {
   257  		stateIdSuffix = fmt.Sprintf(" [%s=%s]", k, v)
   258  	}
   259  
   260  	addrStr := addr.String()
   261  	if depKey, ok := gen.(states.DeposedKey); ok {
   262  		addrStr = fmt.Sprintf("%s (deposed object %s)", addrStr, depKey)
   263  	}
   264  
   265  	h.println(fmt.Sprintf(
   266  		h.view.colorize.Color("[reset][bold]%s: Refreshing state...%s"),
   267  		addrStr, stateIdSuffix))
   268  	return terraform.HookActionContinue, nil
   269  }
   270  
   271  func (h *UiHook) PreImportState(addr addrs.AbsResourceInstance, importID string) (terraform.HookAction, error) {
   272  	h.println(fmt.Sprintf(
   273  		h.view.colorize.Color("[reset][bold]%s: Importing from ID %q..."),
   274  		addr, importID,
   275  	))
   276  	return terraform.HookActionContinue, nil
   277  }
   278  
   279  func (h *UiHook) PostImportState(addr addrs.AbsResourceInstance, imported []providers.ImportedResource) (terraform.HookAction, error) {
   280  	h.println(fmt.Sprintf(
   281  		h.view.colorize.Color("[reset][bold][green]%s: Import prepared!"),
   282  		addr,
   283  	))
   284  	for _, s := range imported {
   285  		h.println(fmt.Sprintf(
   286  			h.view.colorize.Color("[reset][green]  Prepared %s for import"),
   287  			s.TypeName,
   288  		))
   289  	}
   290  
   291  	return terraform.HookActionContinue, nil
   292  }
   293  
   294  // Wrap calls to the view so that concurrent calls do not interleave println.
   295  func (h *UiHook) println(s string) {
   296  	h.viewLock.Lock()
   297  	defer h.viewLock.Unlock()
   298  	h.view.streams.Println(s)
   299  }
   300  
   301  // scanLines is basically copied from the Go standard library except
   302  // we've modified it to also fine `\r`.
   303  func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
   304  	if atEOF && len(data) == 0 {
   305  		return 0, nil, nil
   306  	}
   307  	if i := bytes.IndexByte(data, '\n'); i >= 0 {
   308  		// We have a full newline-terminated line.
   309  		return i + 1, dropCR(data[0:i]), nil
   310  	}
   311  	if i := bytes.IndexByte(data, '\r'); i >= 0 {
   312  		// We have a full carriage-return-terminated line.
   313  		return i + 1, dropCR(data[0:i]), nil
   314  	}
   315  	// If we're at EOF, we have a final, non-terminated line. Return it.
   316  	if atEOF {
   317  		return len(data), dropCR(data), nil
   318  	}
   319  	// Request more data.
   320  	return 0, nil, nil
   321  }
   322  
   323  // dropCR drops a terminal \r from the data.
   324  func dropCR(data []byte) []byte {
   325  	if len(data) > 0 && data[len(data)-1] == '\r' {
   326  		return data[0 : len(data)-1]
   327  	}
   328  	return data
   329  }
   330  
   331  func truncateId(id string, maxLen int) string {
   332  	// Note that the id may contain multibyte characters.
   333  	// We need to truncate it to maxLen characters, not maxLen bytes.
   334  	rid := []rune(id)
   335  	totalLength := len(rid)
   336  	if totalLength <= maxLen {
   337  		return id
   338  	}
   339  	if maxLen < 5 {
   340  		// We don't shorten to less than 5 chars
   341  		// as that would be pointless with ... (3 chars)
   342  		maxLen = 5
   343  	}
   344  
   345  	dots := []rune("...")
   346  	partLen := maxLen / 2
   347  
   348  	leftIdx := partLen - 1
   349  	leftPart := rid[0:leftIdx]
   350  
   351  	rightIdx := totalLength - partLen - 1
   352  
   353  	overlap := maxLen - (partLen*2 + len(dots))
   354  	if overlap < 0 {
   355  		rightIdx -= overlap
   356  	}
   357  
   358  	rightPart := rid[rightIdx:]
   359  
   360  	return string(leftPart) + string(dots) + string(rightPart)
   361  }