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