github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/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/mitchellh/cli"
    14  	"github.com/mitchellh/colorstring"
    15  	"github.com/zclconf/go-cty/cty"
    16  
    17  	"github.com/hashicorp/terraform/addrs"
    18  	"github.com/hashicorp/terraform/command/format"
    19  	"github.com/hashicorp/terraform/plans"
    20  	"github.com/hashicorp/terraform/providers"
    21  	"github.com/hashicorp/terraform/states"
    22  	"github.com/hashicorp/terraform/terraform"
    23  )
    24  
    25  const defaultPeriodicUiTimer = 10 * time.Second
    26  const maxIdLen = 80
    27  
    28  type UiHook struct {
    29  	terraform.NilHook
    30  
    31  	Colorize        *colorstring.Colorize
    32  	Ui              cli.Ui
    33  	PeriodicUiTimer time.Duration
    34  
    35  	l         sync.Mutex
    36  	once      sync.Once
    37  	resources map[string]uiResourceState
    38  	ui        cli.Ui
    39  }
    40  
    41  var _ terraform.Hook = (*UiHook)(nil)
    42  
    43  // uiResourceState tracks the state of a single resource
    44  type uiResourceState struct {
    45  	DispAddr       string
    46  	IDKey, IDValue string
    47  	Op             uiResourceOp
    48  	Start          time.Time
    49  
    50  	DoneCh chan struct{} // To be used for cancellation
    51  
    52  	done chan struct{} // used to coordinate tests
    53  }
    54  
    55  // uiResourceOp is an enum for operations on a resource
    56  type uiResourceOp byte
    57  
    58  const (
    59  	uiResourceUnknown uiResourceOp = iota
    60  	uiResourceCreate
    61  	uiResourceModify
    62  	uiResourceDestroy
    63  )
    64  
    65  func (h *UiHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) {
    66  	h.once.Do(h.init)
    67  
    68  	dispAddr := addr.String()
    69  	if gen != states.CurrentGen {
    70  		dispAddr = fmt.Sprintf("%s (%s)", dispAddr, gen)
    71  	}
    72  
    73  	var operation string
    74  	var op uiResourceOp
    75  	idKey, idValue := format.ObjectValueIDOrName(priorState)
    76  	switch action {
    77  	case plans.Delete:
    78  		operation = "Destroying..."
    79  		op = uiResourceDestroy
    80  	case plans.Create:
    81  		operation = "Creating..."
    82  		op = uiResourceCreate
    83  	case plans.Update:
    84  		operation = "Modifying..."
    85  		op = uiResourceModify
    86  	default:
    87  		// We don't expect any other actions in here, so anything else is a
    88  		// bug in the caller but we'll ignore it in order to be robust.
    89  		h.ui.Output(fmt.Sprintf("(Unknown action %s for %s)", action, dispAddr))
    90  		return terraform.HookActionContinue, nil
    91  	}
    92  
    93  	attrBuf := new(bytes.Buffer)
    94  
    95  	// Get all the attributes that are changing, and sort them. Also
    96  	// determine the longest key so that we can align them all.
    97  	keyLen := 0
    98  
    99  	// FIXME: This is stubbed out in preparation for rewriting it to use
   100  	// a structural presentation rather than the old-style flatmap one.
   101  	// We just assume no attributes at all for now, pending new code to
   102  	// work with the two cty.Values we are given.
   103  	dAttrs := map[string]terraform.ResourceAttrDiff{}
   104  	keys := make([]string, 0, len(dAttrs))
   105  	for key, _ := range dAttrs {
   106  		keys = append(keys, key)
   107  		if len(key) > keyLen {
   108  			keyLen = len(key)
   109  		}
   110  	}
   111  	sort.Strings(keys)
   112  
   113  	// Go through and output each attribute
   114  	for _, attrK := range keys {
   115  		attrDiff := dAttrs[attrK]
   116  
   117  		v := attrDiff.New
   118  		u := attrDiff.Old
   119  		if attrDiff.NewComputed {
   120  			v = "<computed>"
   121  		}
   122  
   123  		if attrDiff.Sensitive {
   124  			u = "<sensitive>"
   125  			v = "<sensitive>"
   126  		}
   127  
   128  		attrBuf.WriteString(fmt.Sprintf(
   129  			"  %s:%s %#v => %#v\n",
   130  			attrK,
   131  			strings.Repeat(" ", keyLen-len(attrK)),
   132  			u,
   133  			v))
   134  	}
   135  
   136  	attrString := strings.TrimSpace(attrBuf.String())
   137  	if attrString != "" {
   138  		attrString = "\n  " + attrString
   139  	}
   140  
   141  	var stateIdSuffix string
   142  	if idKey != "" && idValue != "" {
   143  		stateIdSuffix = fmt.Sprintf(" [%s=%s]", idKey, idValue)
   144  	} else {
   145  		// Make sure they are both empty so we can deal with this more
   146  		// easily in the other hook methods.
   147  		idKey = ""
   148  		idValue = ""
   149  	}
   150  
   151  	h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   152  		"[reset][bold]%s: %s%s[reset]%s",
   153  		dispAddr,
   154  		operation,
   155  		stateIdSuffix,
   156  		attrString,
   157  	)))
   158  
   159  	key := addr.String()
   160  	uiState := uiResourceState{
   161  		DispAddr: key,
   162  		IDKey:    idKey,
   163  		IDValue:  idValue,
   164  		Op:       op,
   165  		Start:    time.Now().Round(time.Second),
   166  		DoneCh:   make(chan struct{}),
   167  		done:     make(chan struct{}),
   168  	}
   169  
   170  	h.l.Lock()
   171  	h.resources[key] = uiState
   172  	h.l.Unlock()
   173  
   174  	// Start goroutine that shows progress
   175  	go h.stillApplying(uiState)
   176  
   177  	return terraform.HookActionContinue, nil
   178  }
   179  
   180  func (h *UiHook) stillApplying(state uiResourceState) {
   181  	defer close(state.done)
   182  	for {
   183  		select {
   184  		case <-state.DoneCh:
   185  			return
   186  
   187  		case <-time.After(h.PeriodicUiTimer):
   188  			// Timer up, show status
   189  		}
   190  
   191  		var msg string
   192  		switch state.Op {
   193  		case uiResourceModify:
   194  			msg = "Still modifying..."
   195  		case uiResourceDestroy:
   196  			msg = "Still destroying..."
   197  		case uiResourceCreate:
   198  			msg = "Still creating..."
   199  		case uiResourceUnknown:
   200  			return
   201  		}
   202  
   203  		idSuffix := ""
   204  		if state.IDKey != "" {
   205  			idSuffix = fmt.Sprintf("%s=%s, ", state.IDKey, truncateId(state.IDValue, maxIdLen))
   206  		}
   207  
   208  		h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   209  			"[reset][bold]%s: %s [%s%s elapsed][reset]",
   210  			state.DispAddr,
   211  			msg,
   212  			idSuffix,
   213  			time.Now().Round(time.Second).Sub(state.Start),
   214  		)))
   215  	}
   216  }
   217  
   218  func (h *UiHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, applyerr error) (terraform.HookAction, error) {
   219  
   220  	id := addr.String()
   221  
   222  	h.l.Lock()
   223  	state := h.resources[id]
   224  	if state.DoneCh != nil {
   225  		close(state.DoneCh)
   226  	}
   227  
   228  	delete(h.resources, id)
   229  	h.l.Unlock()
   230  
   231  	var stateIdSuffix string
   232  	if k, v := format.ObjectValueID(newState); k != "" && v != "" {
   233  		stateIdSuffix = fmt.Sprintf(" [%s=%s]", k, v)
   234  	}
   235  
   236  	var msg string
   237  	switch state.Op {
   238  	case uiResourceModify:
   239  		msg = "Modifications complete"
   240  	case uiResourceDestroy:
   241  		msg = "Destruction complete"
   242  	case uiResourceCreate:
   243  		msg = "Creation complete"
   244  	case uiResourceUnknown:
   245  		return terraform.HookActionContinue, nil
   246  	}
   247  
   248  	if applyerr != nil {
   249  		// Errors are collected and printed in ApplyCommand, no need to duplicate
   250  		return terraform.HookActionContinue, nil
   251  	}
   252  
   253  	colorized := h.Colorize.Color(fmt.Sprintf(
   254  		"[reset][bold]%s: %s after %s%s[reset]",
   255  		addr, msg, time.Now().Round(time.Second).Sub(state.Start), stateIdSuffix))
   256  
   257  	h.ui.Output(colorized)
   258  
   259  	return terraform.HookActionContinue, nil
   260  }
   261  
   262  func (h *UiHook) PreDiff(addr addrs.AbsResourceInstance, gen states.Generation, priorState, proposedNewState cty.Value) (terraform.HookAction, error) {
   263  	return terraform.HookActionContinue, nil
   264  }
   265  
   266  func (h *UiHook) PreProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string) (terraform.HookAction, error) {
   267  	h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   268  		"[reset][bold]%s: Provisioning with '%s'...[reset]",
   269  		addr, typeName,
   270  	)))
   271  	return terraform.HookActionContinue, nil
   272  }
   273  
   274  func (h *UiHook) ProvisionOutput(addr addrs.AbsResourceInstance, typeName string, msg string) {
   275  	var buf bytes.Buffer
   276  	buf.WriteString(h.Colorize.Color("[reset]"))
   277  
   278  	prefix := fmt.Sprintf("%s (%s): ", addr, typeName)
   279  	s := bufio.NewScanner(strings.NewReader(msg))
   280  	s.Split(scanLines)
   281  	for s.Scan() {
   282  		line := strings.TrimRightFunc(s.Text(), unicode.IsSpace)
   283  		if line != "" {
   284  			buf.WriteString(fmt.Sprintf("%s%s\n", prefix, line))
   285  		}
   286  	}
   287  
   288  	h.ui.Output(strings.TrimSpace(buf.String()))
   289  }
   290  
   291  func (h *UiHook) PreRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value) (terraform.HookAction, error) {
   292  	h.once.Do(h.init)
   293  
   294  	var stateIdSuffix string
   295  	if k, v := format.ObjectValueID(priorState); k != "" && v != "" {
   296  		stateIdSuffix = fmt.Sprintf(" [%s=%s]", k, v)
   297  	}
   298  
   299  	h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   300  		"[reset][bold]%s: Refreshing state...%s",
   301  		addr, stateIdSuffix)))
   302  	return terraform.HookActionContinue, nil
   303  }
   304  
   305  func (h *UiHook) PreImportState(addr addrs.AbsResourceInstance, importID string) (terraform.HookAction, error) {
   306  	h.once.Do(h.init)
   307  
   308  	h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   309  		"[reset][bold]%s: Importing from ID %q...",
   310  		addr, importID,
   311  	)))
   312  	return terraform.HookActionContinue, nil
   313  }
   314  
   315  func (h *UiHook) PostImportState(addr addrs.AbsResourceInstance, imported []providers.ImportedResource) (terraform.HookAction, error) {
   316  	h.once.Do(h.init)
   317  
   318  	h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   319  		"[reset][bold][green]%s: Import prepared!", addr)))
   320  	for _, s := range imported {
   321  		h.ui.Output(h.Colorize.Color(fmt.Sprintf(
   322  			"[reset][green]  Prepared %s for import",
   323  			s.TypeName,
   324  		)))
   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  	// Note that the id may contain multibyte characters.
   377  	// We need to truncate it to maxLen characters, not maxLen bytes.
   378  	rid := []rune(id)
   379  	totalLength := len(rid)
   380  	if totalLength <= maxLen {
   381  		return id
   382  	}
   383  	if maxLen < 5 {
   384  		// We don't shorten to less than 5 chars
   385  		// as that would be pointless with ... (3 chars)
   386  		maxLen = 5
   387  	}
   388  
   389  	dots := []rune("...")
   390  	partLen := maxLen / 2
   391  
   392  	leftIdx := partLen - 1
   393  	leftPart := rid[0:leftIdx]
   394  
   395  	rightIdx := totalLength - partLen - 1
   396  
   397  	overlap := maxLen - (partLen*2 + len(dots))
   398  	if overlap < 0 {
   399  		rightIdx -= overlap
   400  	}
   401  
   402  	rightPart := rid[rightIdx:]
   403  
   404  	return string(leftPart) + string(dots) + string(rightPart)
   405  }