github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/backend/display/progress.go (about)

     1  // Copyright 2016-2018, Pulumi Corporation.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // nolint: goconst
    16  package display
    17  
    18  import (
    19  	"bytes"
    20  	"fmt"
    21  	"io"
    22  	"os"
    23  	"runtime"
    24  	"sort"
    25  	"strings"
    26  	"time"
    27  	"unicode"
    28  
    29  	"github.com/pulumi/pulumi/pkg/v3/backend/display/internal/terminal"
    30  	"github.com/pulumi/pulumi/pkg/v3/engine"
    31  	"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
    32  	"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
    33  	"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
    34  	"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
    35  	"github.com/pulumi/pulumi/sdk/v3/go/common/display"
    36  	"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
    37  	"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
    38  	"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
    39  	"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
    40  )
    41  
    42  // DiagInfo contains the bundle of diagnostic information for a single resource.
    43  type DiagInfo struct {
    44  	ErrorCount, WarningCount, InfoCount, DebugCount int
    45  
    46  	// The very last diagnostic event we got for this resource (regardless of severity). We'll print
    47  	// this out in the non-interactive mode whenever we get new events. Importantly, we don't want
    48  	// to print out the most significant diagnostic, as that means a flurry of event swill cause us
    49  	// to keep printing out the most significant diagnostic over and over again.
    50  	LastDiag *engine.DiagEventPayload
    51  
    52  	// The last error we received.  If we have an error, and we're in tree-view, we'll prefer to
    53  	// show this over the last non-error diag so that users know about something bad early on.
    54  	LastError *engine.DiagEventPayload
    55  
    56  	// All the diagnostic events we've heard about this resource.  We'll print the last diagnostic
    57  	// in the status region while a resource is in progress.  At the end we'll print out all
    58  	// diagnostics for a resource.
    59  	//
    60  	// Diagnostic events are bucketed by their associated stream ID (with 0 being the default
    61  	// stream).
    62  	StreamIDToDiagPayloads map[int32][]engine.DiagEventPayload
    63  }
    64  
    65  type progressRenderer interface {
    66  	io.Closer
    67  
    68  	tick(display *ProgressDisplay)
    69  	rowUpdated(display *ProgressDisplay, row Row)
    70  	systemMessage(display *ProgressDisplay, payload engine.StdoutEventPayload)
    71  	done(display *ProgressDisplay)
    72  	println(display *ProgressDisplay, line string)
    73  }
    74  
    75  // ProgressDisplay organizes all the information needed for a dynamically updated "progress" view of an update.
    76  type ProgressDisplay struct {
    77  	opts Options
    78  
    79  	renderer progressRenderer
    80  
    81  	// action is the kind of action (preview, update, refresh, etc) being performed.
    82  	action apitype.UpdateKind
    83  	// stack is the stack this progress pertains to.
    84  	stack tokens.Name
    85  	// proj is the project this progress pertains to.
    86  	proj tokens.PackageName
    87  
    88  	// Whether or not we're previewing.  We don't know what we are actually doing until
    89  	// we get the initial 'prelude' event.
    90  	//
    91  	// this flag is only used to adjust how we describe what's going on to the user.
    92  	// i.e. if we're previewing we say things like "Would update" instead of "Updating".
    93  	isPreview bool
    94  
    95  	// The urn of the stack.
    96  	stackUrn resource.URN
    97  
    98  	// Whether or not we've seen outputs for the stack yet.
    99  	seenStackOutputs bool
   100  
   101  	// The summary event from the engine.  If we get this, we'll print this after all
   102  	// normal resource events are heard.  That way we don't interfere with all the progress
   103  	// messages we're outputting for them.
   104  	summaryEventPayload *engine.SummaryEventPayload
   105  
   106  	// Any system events we've received.  They will be printed at the bottom of all the status rows
   107  	systemEventPayloads []engine.StdoutEventPayload
   108  
   109  	// Used to record the order that rows are created in.  That way, when we present in a tree, we
   110  	// can keep things ordered so they will not jump around.
   111  	displayOrderCounter int
   112  
   113  	// What tick we're currently on.  Used to determine the number of ellipses to concat to
   114  	// a status message to help indicate that things are still working.
   115  	currentTick int
   116  
   117  	headerRow    Row
   118  	resourceRows []ResourceRow
   119  
   120  	// A mapping from each resource URN we are told about to its current status.
   121  	eventUrnToResourceRow map[resource.URN]ResourceRow
   122  
   123  	// Remember if we're a terminal or not.  In a terminal we get a little bit fancier.
   124  	// For example, we'll go back and update previous status messages to make sure things
   125  	// align.  We don't need to do that in non-terminal situations.
   126  	isTerminal bool
   127  
   128  	// If all progress messages are done and we can print out the final display.
   129  	done bool
   130  
   131  	// The column that the suffix should be added to
   132  	suffixColumn int
   133  
   134  	// the list of suffixes to rotate through
   135  	suffixesArray []string
   136  
   137  	// Maps used so we can generate short IDs for resource urns.
   138  	urnToID map[resource.URN]string
   139  
   140  	// Structure that tracks the time taken to perform an action on a resource.
   141  	opStopwatch opStopwatch
   142  }
   143  
   144  type opStopwatch struct {
   145  	start map[resource.URN]time.Time
   146  	end   map[resource.URN]time.Time
   147  }
   148  
   149  func newOpStopwatch() opStopwatch {
   150  	return opStopwatch{
   151  		start: map[resource.URN]time.Time{},
   152  		end:   map[resource.URN]time.Time{},
   153  	}
   154  }
   155  
   156  var (
   157  	// policyPayloads is a collection of policy violation events for a single resource.
   158  	policyPayloads []engine.PolicyViolationEventPayload
   159  )
   160  
   161  func camelCase(s string) string {
   162  	if len(s) == 0 {
   163  		return s
   164  	}
   165  
   166  	runes := []rune(s)
   167  	runes[0] = unicode.ToLower(runes[0])
   168  	return string(runes)
   169  }
   170  
   171  func simplifyTypeName(typ tokens.Type) string {
   172  	typeString := string(typ)
   173  
   174  	components := strings.Split(typeString, ":")
   175  	if len(components) != 3 {
   176  		return typeString
   177  	}
   178  	pkg, module, name := components[0], components[1], components[2]
   179  
   180  	if len(name) == 0 {
   181  		return typeString
   182  	}
   183  
   184  	lastSlashInModule := strings.LastIndexByte(module, '/')
   185  	if lastSlashInModule == -1 {
   186  		return typeString
   187  	}
   188  	file := module[lastSlashInModule+1:]
   189  
   190  	if file != camelCase(name) {
   191  		return typeString
   192  	}
   193  
   194  	return fmt.Sprintf("%v:%v:%v", pkg, module[:lastSlashInModule], name)
   195  }
   196  
   197  // getEventUrn returns the resource URN associated with an event, or the empty URN if this is not an
   198  // event that has a URN.  If this is also a 'step' event, then this will return the step metadata as
   199  // well.
   200  func getEventUrnAndMetadata(event engine.Event) (resource.URN, *engine.StepEventMetadata) {
   201  	switch event.Type {
   202  	case engine.ResourcePreEvent:
   203  		payload := event.Payload().(engine.ResourcePreEventPayload)
   204  		return payload.Metadata.URN, &payload.Metadata
   205  	case engine.ResourceOutputsEvent:
   206  		payload := event.Payload().(engine.ResourceOutputsEventPayload)
   207  		return payload.Metadata.URN, &payload.Metadata
   208  	case engine.ResourceOperationFailed:
   209  		payload := event.Payload().(engine.ResourceOperationFailedPayload)
   210  		return payload.Metadata.URN, &payload.Metadata
   211  	case engine.DiagEvent:
   212  		return event.Payload().(engine.DiagEventPayload).URN, nil
   213  	case engine.PolicyViolationEvent:
   214  		return event.Payload().(engine.PolicyViolationEventPayload).ResourceURN, nil
   215  	default:
   216  		return "", nil
   217  	}
   218  }
   219  
   220  // ShowProgressEvents displays the engine events with docker's progress view.
   221  func ShowProgressEvents(op string, action apitype.UpdateKind, stack tokens.Name, proj tokens.PackageName,
   222  	events <-chan engine.Event, done chan<- bool, opts Options, isPreview bool) {
   223  
   224  	stdin := opts.Stdin
   225  	if stdin == nil {
   226  		stdin = os.Stdin
   227  	}
   228  	stdout := opts.Stdout
   229  	if stdout == nil {
   230  		stdout = os.Stdout
   231  	}
   232  	stderr := opts.Stderr
   233  	if stderr == nil {
   234  		stderr = os.Stderr
   235  	}
   236  
   237  	isInteractive, term := opts.IsInteractive, opts.term
   238  	if isInteractive && term == nil {
   239  		raw := runtime.GOOS != "windows"
   240  		t, err := terminal.Open(stdin, stdout, raw)
   241  		if err != nil {
   242  			_, err = fmt.Fprintln(stderr, "Failed to open terminal; treating display as non-interactive (%w)", err)
   243  			contract.IgnoreError(err)
   244  			isInteractive = false
   245  		} else {
   246  			term = t
   247  		}
   248  	}
   249  
   250  	var renderer progressRenderer
   251  	if isInteractive {
   252  		renderer = newInteractiveRenderer(term, opts)
   253  	} else {
   254  		renderer = newNonInteractiveRenderer(stdout, op, opts)
   255  	}
   256  
   257  	display := &ProgressDisplay{
   258  		action:                action,
   259  		isPreview:             isPreview,
   260  		isTerminal:            isInteractive,
   261  		opts:                  opts,
   262  		renderer:              renderer,
   263  		stack:                 stack,
   264  		proj:                  proj,
   265  		eventUrnToResourceRow: make(map[resource.URN]ResourceRow),
   266  		suffixColumn:          int(statusColumn),
   267  		suffixesArray:         []string{"", ".", "..", "..."},
   268  		urnToID:               make(map[resource.URN]string),
   269  		displayOrderCounter:   1,
   270  		opStopwatch:           newOpStopwatch(),
   271  	}
   272  
   273  	ticker := time.NewTicker(1 * time.Second)
   274  	if opts.deterministicOutput {
   275  		ticker.Stop()
   276  	}
   277  	display.processEvents(ticker, events)
   278  	contract.IgnoreClose(display.renderer)
   279  	ticker.Stop()
   280  
   281  	// let our caller know we're done.
   282  	close(done)
   283  }
   284  
   285  func (display *ProgressDisplay) println(line string) {
   286  	display.renderer.println(display, line)
   287  }
   288  
   289  type treeNode struct {
   290  	row Row
   291  
   292  	colorizedColumns []string
   293  	colorizedSuffix  string
   294  
   295  	childNodes []*treeNode
   296  }
   297  
   298  func (display *ProgressDisplay) getOrCreateTreeNode(
   299  	result *[]*treeNode, urn resource.URN, row ResourceRow, urnToTreeNode map[resource.URN]*treeNode) *treeNode {
   300  
   301  	node, has := urnToTreeNode[urn]
   302  	if has {
   303  		return node
   304  	}
   305  
   306  	node = &treeNode{
   307  		row:              row,
   308  		colorizedColumns: row.ColorizedColumns(),
   309  		colorizedSuffix:  row.ColorizedSuffix(),
   310  	}
   311  
   312  	urnToTreeNode[urn] = node
   313  
   314  	// if it's the not the root item, attach it as a child node to an appropriate parent item.
   315  	if urn != "" && urn != display.stackUrn {
   316  		var parentURN resource.URN
   317  
   318  		res := row.Step().Res
   319  		if res != nil {
   320  			parentURN = res.Parent
   321  		}
   322  
   323  		parentRow, hasParentRow := display.eventUrnToResourceRow[parentURN]
   324  
   325  		if !hasParentRow {
   326  			// If we haven't heard about this node's parent, then  just parent it to the stack.
   327  			// Note: getting the parent row for the stack-urn will always succeed as we ensure that
   328  			// such a row is always there in ensureHeaderAndStackRows
   329  			parentURN = display.stackUrn
   330  			parentRow = display.eventUrnToResourceRow[parentURN]
   331  		}
   332  
   333  		parentNode := display.getOrCreateTreeNode(result, parentURN, parentRow, urnToTreeNode)
   334  		parentNode.childNodes = append(parentNode.childNodes, node)
   335  		return node
   336  	}
   337  
   338  	*result = append(*result, node)
   339  	return node
   340  }
   341  
   342  func (display *ProgressDisplay) generateTreeNodes() []*treeNode {
   343  	result := []*treeNode{}
   344  
   345  	result = append(result, &treeNode{
   346  		row:              display.headerRow,
   347  		colorizedColumns: display.headerRow.ColorizedColumns(),
   348  	})
   349  
   350  	urnToTreeNode := make(map[resource.URN]*treeNode)
   351  	for urn, row := range display.eventUrnToResourceRow {
   352  		display.getOrCreateTreeNode(&result, urn, row, urnToTreeNode)
   353  	}
   354  
   355  	return result
   356  }
   357  
   358  func (display *ProgressDisplay) addIndentations(treeNodes []*treeNode, isRoot bool, indentation string) {
   359  	childIndentation := indentation + "│  "
   360  	lastChildIndentation := indentation + "   "
   361  
   362  	for i, node := range treeNodes {
   363  		isLast := i == len(treeNodes)-1
   364  
   365  		prefix := indentation
   366  
   367  		var nestedIndentation string
   368  		if !isRoot {
   369  			if isLast {
   370  				prefix += "└─ "
   371  				nestedIndentation = lastChildIndentation
   372  			} else {
   373  				prefix += "├─ "
   374  				nestedIndentation = childIndentation
   375  			}
   376  		}
   377  
   378  		node.colorizedColumns[typeColumn] = prefix + node.colorizedColumns[typeColumn]
   379  		display.addIndentations(node.childNodes, false /*isRoot*/, nestedIndentation)
   380  	}
   381  }
   382  
   383  func (display *ProgressDisplay) convertNodesToRows(
   384  	nodes []*treeNode, maxSuffixLength int, rows *[][]string, maxColumnLengths *[]int) {
   385  
   386  	for _, node := range nodes {
   387  		if len(*maxColumnLengths) == 0 {
   388  			*maxColumnLengths = make([]int, len(node.colorizedColumns))
   389  		}
   390  
   391  		colorizedColumns := make([]string, len(node.colorizedColumns))
   392  
   393  		for i, colorizedColumn := range node.colorizedColumns {
   394  			columnWidth := colors.MeasureColorizedString(colorizedColumn)
   395  
   396  			if i == display.suffixColumn {
   397  				columnWidth += maxSuffixLength
   398  				colorizedColumns[i] = colorizedColumn + node.colorizedSuffix
   399  			} else {
   400  				colorizedColumns[i] = colorizedColumn
   401  			}
   402  
   403  			if columnWidth > (*maxColumnLengths)[i] {
   404  				(*maxColumnLengths)[i] = columnWidth
   405  			}
   406  		}
   407  
   408  		*rows = append(*rows, colorizedColumns)
   409  
   410  		display.convertNodesToRows(node.childNodes, maxSuffixLength, rows, maxColumnLengths)
   411  	}
   412  }
   413  
   414  type sortable []*treeNode
   415  
   416  func (sortable sortable) Len() int {
   417  	return len(sortable)
   418  }
   419  
   420  func (sortable sortable) Less(i, j int) bool {
   421  	return sortable[i].row.DisplayOrderIndex() < sortable[j].row.DisplayOrderIndex()
   422  }
   423  
   424  func (sortable sortable) Swap(i, j int) {
   425  	sortable[i], sortable[j] = sortable[j], sortable[i]
   426  }
   427  
   428  func sortNodes(nodes []*treeNode) {
   429  	sort.Sort(sortable(nodes))
   430  
   431  	for _, node := range nodes {
   432  		childNodes := node.childNodes
   433  		sortNodes(childNodes)
   434  		node.childNodes = childNodes
   435  	}
   436  }
   437  
   438  func (display *ProgressDisplay) filterOutUnnecessaryNodesAndSetDisplayTimes(nodes []*treeNode) []*treeNode {
   439  	result := []*treeNode{}
   440  
   441  	for _, node := range nodes {
   442  		node.childNodes = display.filterOutUnnecessaryNodesAndSetDisplayTimes(node.childNodes)
   443  
   444  		if node.row.HideRowIfUnnecessary() && len(node.childNodes) == 0 {
   445  			continue
   446  		}
   447  
   448  		display.displayOrderCounter++
   449  		node.row.SetDisplayOrderIndex(display.displayOrderCounter)
   450  		result = append(result, node)
   451  	}
   452  
   453  	return result
   454  }
   455  
   456  func removeInfoColumnIfUnneeded(rows [][]string) {
   457  	// If there have been no info messages, then don't print out the info column header.
   458  	for i := 1; i < len(rows); i++ {
   459  		row := rows[i]
   460  		if row[len(row)-1] != "" {
   461  			return
   462  		}
   463  	}
   464  
   465  	firstRow := rows[0]
   466  	firstRow[len(firstRow)-1] = ""
   467  }
   468  
   469  // Performs all the work at the end once we've heard about the last message from the engine.
   470  // Specifically, this will update the status messages for any resources, and will also then
   471  // print out all final diagnostics. and finally will print out the summary.
   472  func (display *ProgressDisplay) processEndSteps() {
   473  	// Figure out the rows that are currently in progress.
   474  	var inProgressRows []ResourceRow
   475  	if !display.isTerminal {
   476  		for _, v := range display.eventUrnToResourceRow {
   477  			if !v.IsDone() {
   478  				inProgressRows = append(inProgressRows, v)
   479  			}
   480  		}
   481  	}
   482  
   483  	// Transition the display to the 'done' state.  This will transitively cause all
   484  	// rows to become done.
   485  	display.done = true
   486  
   487  	// Now print out all those rows that were in progress.  They will now be 'done'
   488  	// since the display was marked 'done'.
   489  	if !display.isTerminal {
   490  		for _, v := range inProgressRows {
   491  			display.renderer.rowUpdated(display, v)
   492  		}
   493  	}
   494  
   495  	// Now refresh everything.  This ensures that we go back and remove things like the diagnostic
   496  	// messages from a status message (since we're going to print them all) below.  Note, this will
   497  	// only do something in a terminal.  This is what we want, because if we're not in a terminal we
   498  	// don't really want to reprint any finished items we've already printed.
   499  	display.renderer.done(display)
   500  
   501  	// Render several "sections" of output based on available data as applicable.
   502  	display.println("")
   503  	wroteDiagnosticHeader := display.printDiagnostics()
   504  	wrotePolicyViolations := display.printPolicyViolations()
   505  	display.printOutputs()
   506  	// If no policies violated, print policy packs applied.
   507  	if !wrotePolicyViolations {
   508  		display.printSummary(wroteDiagnosticHeader)
   509  	}
   510  }
   511  
   512  // printDiagnostics prints a new "Diagnostics:" section with all of the diagnostics grouped by
   513  // resource. If no diagnostics were emitted, prints nothing.
   514  func (display *ProgressDisplay) printDiagnostics() bool {
   515  	// Since we display diagnostic information eagerly, we need to keep track of the first
   516  	// time we wrote some output so we don't inadvertently print the header twice.
   517  	wroteDiagnosticHeader := false
   518  	for _, row := range display.eventUrnToResourceRow {
   519  		// The header for the diagnogistics grouped by resource, e.g. "aws:apigateway:RestApi (accountsApi):"
   520  		wroteResourceHeader := false
   521  
   522  		// Each row in the display corresponded with a resource, and that resource could have emitted
   523  		// diagnostics to various streams.
   524  		for id, payloads := range row.DiagInfo().StreamIDToDiagPayloads {
   525  			if len(payloads) == 0 {
   526  				continue
   527  			}
   528  
   529  			if id != 0 {
   530  				// For the non-default stream merge all the messages from the stream into a single
   531  				// message.
   532  				p := display.mergeStreamPayloadsToSinglePayload(payloads)
   533  				payloads = []engine.DiagEventPayload{p}
   534  			}
   535  
   536  			// Did we write any diagnostic information for the resource x stream?
   537  			wrote := false
   538  			for _, v := range payloads {
   539  				if v.Ephemeral {
   540  					continue
   541  				}
   542  
   543  				msg := display.renderProgressDiagEvent(v, true /*includePrefix:*/)
   544  
   545  				lines := splitIntoDisplayableLines(msg)
   546  				if len(lines) == 0 {
   547  					continue
   548  				}
   549  
   550  				// If we haven't printed the Diagnostics header, do so now.
   551  				if !wroteDiagnosticHeader {
   552  					wroteDiagnosticHeader = true
   553  					display.println(colors.SpecHeadline + "Diagnostics:" + colors.Reset)
   554  				}
   555  				// If we haven't printed the header for the resource, do so now.
   556  				if !wroteResourceHeader {
   557  					wroteResourceHeader = true
   558  					columns := row.ColorizedColumns()
   559  					display.println(
   560  						"  " + colors.BrightBlue + columns[typeColumn] + " (" + columns[nameColumn] + "):" + colors.Reset)
   561  				}
   562  
   563  				for _, line := range lines {
   564  					line = strings.TrimRightFunc(line, unicode.IsSpace)
   565  					display.println("    " + line)
   566  				}
   567  
   568  				wrote = true
   569  			}
   570  
   571  			if wrote {
   572  				display.println("")
   573  			}
   574  		}
   575  
   576  	}
   577  	return wroteDiagnosticHeader
   578  }
   579  
   580  // printPolicyViolations prints a new "Policy Violation:" section with all of the violations
   581  // grouped by policy pack. If no policy violations were encountered, prints nothing.
   582  func (display *ProgressDisplay) printPolicyViolations() bool {
   583  	// Loop through every resource and gather up all policy violations encountered.
   584  	var policyEvents []engine.PolicyViolationEventPayload
   585  	for _, row := range display.eventUrnToResourceRow {
   586  		policyPayloads := row.PolicyPayloads()
   587  		if len(policyPayloads) == 0 {
   588  			continue
   589  		}
   590  		policyEvents = append(policyEvents, policyPayloads...)
   591  	}
   592  	if len(policyEvents) == 0 {
   593  		return false
   594  	}
   595  	// Sort policy events by: policy pack name, policy pack version, enforcement level,
   596  	// policy name, and finally the URN of the resource.
   597  	sort.SliceStable(policyEvents, func(i, j int) bool {
   598  		eventI, eventJ := policyEvents[i], policyEvents[j]
   599  		if packNameCmp := strings.Compare(
   600  			eventI.PolicyPackName,
   601  			eventJ.PolicyPackName); packNameCmp != 0 {
   602  			return packNameCmp < 0
   603  		}
   604  		if packVerCmp := strings.Compare(
   605  			eventI.PolicyPackVersion,
   606  			eventJ.PolicyPackVersion); packVerCmp != 0 {
   607  			return packVerCmp < 0
   608  		}
   609  		if enfLevelCmp := strings.Compare(
   610  			string(eventI.EnforcementLevel),
   611  			string(eventJ.EnforcementLevel)); enfLevelCmp != 0 {
   612  			return enfLevelCmp < 0
   613  		}
   614  		if policyNameCmp := strings.Compare(
   615  			eventI.PolicyName,
   616  			eventJ.PolicyName); policyNameCmp != 0 {
   617  			return policyNameCmp < 0
   618  		}
   619  		urnCmp := strings.Compare(
   620  			string(eventI.ResourceURN),
   621  			string(eventJ.ResourceURN))
   622  		return urnCmp < 0
   623  	})
   624  
   625  	// Print every policy violation, printing a new header when necessary.
   626  	display.println(display.opts.Color.Colorize(colors.SpecHeadline + "Policy Violations:" + colors.Reset))
   627  
   628  	for _, policyEvent := range policyEvents {
   629  		// Print the individual policy event.
   630  		c := colors.SpecImportant
   631  		if policyEvent.EnforcementLevel == apitype.Mandatory {
   632  			c = colors.SpecError
   633  		}
   634  
   635  		policyNameLine := fmt.Sprintf("    %s[%s]  %s v%s %s %s (%s: %s)",
   636  			c, policyEvent.EnforcementLevel,
   637  			policyEvent.PolicyPackName,
   638  			policyEvent.PolicyPackVersion, colors.Reset,
   639  			policyEvent.PolicyName,
   640  			policyEvent.ResourceURN.Type(),
   641  			policyEvent.ResourceURN.Name())
   642  		display.println(policyNameLine)
   643  
   644  		// The message may span multiple lines, so we massage it so it will be indented properly.
   645  		message := strings.ReplaceAll(policyEvent.Message, "\n", "\n    ")
   646  		messageLine := fmt.Sprintf("    %s", message)
   647  		display.println(messageLine)
   648  	}
   649  	return true
   650  }
   651  
   652  // printOutputs prints the Stack's outputs for the display in a new section, if appropriate.
   653  func (display *ProgressDisplay) printOutputs() {
   654  	// Printing the stack's outputs wasn't desired.
   655  	if display.opts.SuppressOutputs {
   656  		return
   657  	}
   658  	// Cannot display outputs for the stack if we don't know its URN.
   659  	if display.stackUrn == "" {
   660  		return
   661  	}
   662  
   663  	stackStep := display.eventUrnToResourceRow[display.stackUrn].Step()
   664  
   665  	props := getResourceOutputsPropertiesString(
   666  		stackStep, 1, display.isPreview, display.opts.Debug,
   667  		false /* refresh */, display.opts.ShowSameResources)
   668  	if props != "" {
   669  		display.println(colors.SpecHeadline + "Outputs:" + colors.Reset)
   670  		display.println(props)
   671  	}
   672  }
   673  
   674  // printSummary prints the Stack's SummaryEvent in a new section if applicable.
   675  func (display *ProgressDisplay) printSummary(wroteDiagnosticHeader bool) {
   676  	// If we never saw the SummaryEvent payload, we have nothing to do.
   677  	if display.summaryEventPayload == nil {
   678  		return
   679  	}
   680  
   681  	msg := renderSummaryEvent(*display.summaryEventPayload, wroteDiagnosticHeader, display.opts)
   682  	display.println(msg)
   683  }
   684  
   685  func (display *ProgressDisplay) mergeStreamPayloadsToSinglePayload(
   686  	payloads []engine.DiagEventPayload) engine.DiagEventPayload {
   687  	buf := bytes.Buffer{}
   688  
   689  	for _, p := range payloads {
   690  		buf.WriteString(display.renderProgressDiagEvent(p, false /*includePrefix:*/))
   691  	}
   692  
   693  	firstPayload := payloads[0]
   694  	msg := buf.String()
   695  	return engine.DiagEventPayload{
   696  		URN:       firstPayload.URN,
   697  		Message:   msg,
   698  		Prefix:    firstPayload.Prefix,
   699  		Color:     firstPayload.Color,
   700  		Severity:  firstPayload.Severity,
   701  		StreamID:  firstPayload.StreamID,
   702  		Ephemeral: firstPayload.Ephemeral,
   703  	}
   704  }
   705  
   706  func splitIntoDisplayableLines(msg string) []string {
   707  	lines := strings.Split(msg, "\n")
   708  
   709  	// Trim off any trailing blank lines in the message.
   710  	for len(lines) > 0 {
   711  		lastLine := lines[len(lines)-1]
   712  		if strings.TrimSpace(colors.Never.Colorize(lastLine)) == "" {
   713  			lines = lines[0 : len(lines)-1]
   714  		} else {
   715  			break
   716  		}
   717  	}
   718  
   719  	return lines
   720  }
   721  
   722  func (display *ProgressDisplay) processTick() {
   723  	// Got a tick.  Update the progress display if we're in a terminal.  If we're not,
   724  	// print a hearbeat message every 10 seconds after our last output so that the user
   725  	// knows something is going on.  This is also helpful for hosts like jenkins that
   726  	// often timeout a process if output is not seen in a while.
   727  	display.currentTick++
   728  
   729  	display.renderer.tick(display)
   730  }
   731  
   732  func (display *ProgressDisplay) getRowForURN(urn resource.URN, metadata *engine.StepEventMetadata) ResourceRow {
   733  	// If there's already a row for this URN, return it.
   734  	row, has := display.eventUrnToResourceRow[urn]
   735  	if has {
   736  		return row
   737  	}
   738  
   739  	// First time we're hearing about this resource. Create an initial nearly-empty status for it.
   740  	step := engine.StepEventMetadata{URN: urn, Op: deploy.OpSame}
   741  	if metadata != nil {
   742  		step = *metadata
   743  	}
   744  
   745  	// If this is the first time we're seeing an event for the stack resource, check to see if we've already
   746  	// recorded root events that we want to reassociate with this URN.
   747  	if isRootURN(urn) {
   748  		display.stackUrn = urn
   749  
   750  		if row, has = display.eventUrnToResourceRow[""]; has {
   751  			row.SetStep(step)
   752  			display.eventUrnToResourceRow[urn] = row
   753  			delete(display.eventUrnToResourceRow, "")
   754  			return row
   755  		}
   756  	}
   757  
   758  	row = &resourceRowData{
   759  		display:              display,
   760  		tick:                 display.currentTick,
   761  		diagInfo:             &DiagInfo{},
   762  		policyPayloads:       policyPayloads,
   763  		step:                 step,
   764  		hideRowIfUnnecessary: true,
   765  	}
   766  
   767  	display.eventUrnToResourceRow[urn] = row
   768  
   769  	display.ensureHeaderAndStackRows()
   770  	display.resourceRows = append(display.resourceRows, row)
   771  	return row
   772  }
   773  
   774  func (display *ProgressDisplay) processNormalEvent(event engine.Event) {
   775  	switch event.Type {
   776  	case engine.PreludeEvent:
   777  		// A prelude event can just be printed out directly to the console.
   778  		// Note: we should probably make sure we don't get any prelude events
   779  		// once we start hearing about actual resource events.
   780  		payload := event.Payload().(engine.PreludeEventPayload)
   781  		preludeEventString := renderPreludeEvent(payload, display.opts)
   782  		if display.isTerminal {
   783  			display.processNormalEvent(engine.NewEvent(engine.DiagEvent, engine.DiagEventPayload{
   784  				Ephemeral: false,
   785  				Severity:  diag.Info,
   786  				Color:     cmdutil.GetGlobalColorization(),
   787  				Message:   preludeEventString,
   788  			}))
   789  		} else {
   790  			display.println(preludeEventString)
   791  		}
   792  		return
   793  	case engine.SummaryEvent:
   794  		// keep track of the summary event so that we can display it after all other
   795  		// resource-related events we receive.
   796  		payload := event.Payload().(engine.SummaryEventPayload)
   797  		display.summaryEventPayload = &payload
   798  		return
   799  	case engine.DiagEvent:
   800  		msg := display.renderProgressDiagEvent(event.Payload().(engine.DiagEventPayload), true /*includePrefix:*/)
   801  		if msg == "" {
   802  			return
   803  		}
   804  	case engine.StdoutColorEvent:
   805  		display.handleSystemEvent(event.Payload().(engine.StdoutEventPayload))
   806  		return
   807  	}
   808  
   809  	// At this point, all events should relate to resources.
   810  	eventUrn, metadata := getEventUrnAndMetadata(event)
   811  
   812  	// If we're suppressing reads from the tree-view, then convert notifications about reads into
   813  	// ephemeral messages that will go into the info column.
   814  	if metadata != nil && !display.opts.ShowReads {
   815  		if metadata.Op == deploy.OpReadDiscard || metadata.Op == deploy.OpReadReplacement {
   816  			// just flat out ignore read discards/replace.  They're only relevant in the context of
   817  			// 'reads', and we only present reads as an ephemeral diagnostic anyways.
   818  			return
   819  		}
   820  
   821  		if metadata.Op == deploy.OpRead {
   822  			// Don't show reads as operations on a specific resource.  It's an underlying detail
   823  			// that we don't want to clutter up the display with.  However, to help users know
   824  			// what's going on, we can show them as ephemeral diagnostic messages that are
   825  			// associated at the top level with the stack.  That way if things are taking a while,
   826  			// there's insight in the display as to what's going on.
   827  			display.processNormalEvent(engine.NewEvent(engine.DiagEvent, engine.DiagEventPayload{
   828  				Ephemeral: true,
   829  				Severity:  diag.Info,
   830  				Color:     cmdutil.GetGlobalColorization(),
   831  				Message:   fmt.Sprintf("read %v %v", simplifyTypeName(eventUrn.Type()), eventUrn.Name()),
   832  			}))
   833  			return
   834  		}
   835  	}
   836  
   837  	if eventUrn == "" {
   838  		// If this event has no URN, associate it with the stack. Note that there may not yet be a stack resource, in
   839  		// which case this is a no-op.
   840  		eventUrn = display.stackUrn
   841  	}
   842  	isRootEvent := eventUrn == display.stackUrn
   843  
   844  	row := display.getRowForURN(eventUrn, metadata)
   845  
   846  	// Don't bother showing certain events (for example, things that are unchanged). However
   847  	// always show the root 'stack' resource so we can indicate that it's still running, and
   848  	// also so we have something to attach unparented diagnostic events to.
   849  	hideRowIfUnnecessary := metadata != nil && !shouldShow(*metadata, display.opts) && !isRootEvent
   850  	// Always show row if there's a policy violation event. Policy violations prevent resource
   851  	// registration, so if we don't show the row, the violation gets attributed to the stack
   852  	// resource rather than the resources whose policy failed.
   853  	hideRowIfUnnecessary = hideRowIfUnnecessary || event.Type == engine.PolicyViolationEvent
   854  	if !hideRowIfUnnecessary {
   855  		row.SetHideRowIfUnnecessary(false)
   856  	}
   857  
   858  	if event.Type == engine.ResourcePreEvent {
   859  		step := event.Payload().(engine.ResourcePreEventPayload).Metadata
   860  
   861  		// Register the resource update start time to calculate duration
   862  		// and time elapsed.
   863  		display.opStopwatch.start[step.URN] = time.Now()
   864  
   865  		row.SetStep(step)
   866  	} else if event.Type == engine.ResourceOutputsEvent {
   867  		isRefresh := display.getStepOp(row.Step()) == deploy.OpRefresh
   868  		step := event.Payload().(engine.ResourceOutputsEventPayload).Metadata
   869  
   870  		// Register the resource update end time to calculate duration
   871  		// to display.
   872  		display.opStopwatch.end[step.URN] = time.Now()
   873  
   874  		// Is this the stack outputs event? If so, we'll need to print it out at the end of the plan.
   875  		if step.URN == display.stackUrn {
   876  			display.seenStackOutputs = true
   877  		}
   878  
   879  		row.SetStep(step)
   880  		row.AddOutputStep(step)
   881  
   882  		// If we're not in a terminal, we may not want to display this row again: if we're displaying a preview or if
   883  		// this step is a no-op for a custom resource, refreshing this row will simply duplicate its earlier output.
   884  		hasMeaningfulOutput := isRefresh ||
   885  			!display.isPreview && (step.Res == nil || step.Res.Custom && step.Op != deploy.OpSame)
   886  		if !display.isTerminal && !hasMeaningfulOutput {
   887  			return
   888  		}
   889  	} else if event.Type == engine.ResourceOperationFailed {
   890  		row.SetFailed()
   891  	} else if event.Type == engine.DiagEvent {
   892  		// also record this diagnostic so we print it at the end.
   893  		row.RecordDiagEvent(event)
   894  	} else if event.Type == engine.PolicyViolationEvent {
   895  		// also record this policy violation so we print it at the end.
   896  		row.RecordPolicyViolationEvent(event)
   897  	} else {
   898  		contract.Failf("Unhandled event type '%s'", event.Type)
   899  	}
   900  
   901  	display.renderer.rowUpdated(display, row)
   902  }
   903  
   904  func (display *ProgressDisplay) handleSystemEvent(payload engine.StdoutEventPayload) {
   905  	// Make sure we have a header to display
   906  	display.ensureHeaderAndStackRows()
   907  
   908  	display.systemEventPayloads = append(display.systemEventPayloads, payload)
   909  
   910  	display.renderer.systemMessage(display, payload)
   911  }
   912  
   913  func (display *ProgressDisplay) ensureHeaderAndStackRows() {
   914  	if display.headerRow == nil {
   915  		// about to make our first status message.  make sure we present the header line first.
   916  		display.headerRow = &headerRowData{display: display}
   917  	}
   918  
   919  	// we've added at least one row to the table.  make sure we have a row to designate the
   920  	// stack if we haven't already heard about it yet.  This also ensures that as we build
   921  	// the tree we can always guarantee there's a 'root' to parent anything to.
   922  	_, hasStackRow := display.eventUrnToResourceRow[display.stackUrn]
   923  	if hasStackRow {
   924  		return
   925  	}
   926  
   927  	stackRow := &resourceRowData{
   928  		display:              display,
   929  		tick:                 display.currentTick,
   930  		diagInfo:             &DiagInfo{},
   931  		policyPayloads:       policyPayloads,
   932  		step:                 engine.StepEventMetadata{Op: deploy.OpSame},
   933  		hideRowIfUnnecessary: false,
   934  	}
   935  
   936  	display.eventUrnToResourceRow[display.stackUrn] = stackRow
   937  	display.resourceRows = append(display.resourceRows, stackRow)
   938  }
   939  
   940  func (display *ProgressDisplay) processEvents(ticker *time.Ticker, events <-chan engine.Event) {
   941  	// Main processing loop.  The purpose of this func is to read in events from the engine
   942  	// and translate them into Status objects and progress messages to be presented to the
   943  	// command line.
   944  	for {
   945  		select {
   946  		case <-ticker.C:
   947  			display.processTick()
   948  
   949  		case event := <-events:
   950  			if event.Type == "" || event.Type == engine.CancelEvent {
   951  				// Engine finished sending events.  Do all the final processing and return
   952  				// from this local func.  This will print out things like full diagnostic
   953  				// events, as well as the summary event from the engine.
   954  				display.processEndSteps()
   955  				return
   956  			}
   957  
   958  			display.processNormalEvent(event)
   959  		}
   960  	}
   961  }
   962  
   963  func (display *ProgressDisplay) renderProgressDiagEvent(payload engine.DiagEventPayload, includePrefix bool) string {
   964  	if payload.Severity == diag.Debug && !display.opts.Debug {
   965  		return ""
   966  	}
   967  
   968  	msg := payload.Message
   969  	if includePrefix {
   970  		msg = payload.Prefix + msg
   971  	}
   972  
   973  	return strings.TrimRightFunc(msg, unicode.IsSpace)
   974  }
   975  
   976  func (display *ProgressDisplay) getStepDoneDescription(step engine.StepEventMetadata, failed bool) string {
   977  	makeError := func(v string) string {
   978  		return colors.SpecError + "**" + v + "**" + colors.Reset
   979  	}
   980  
   981  	op := display.getStepOp(step)
   982  
   983  	if display.isPreview {
   984  		// During a preview, when we transition to done, we'll print out summary text describing the step instead of a
   985  		// past-tense verb describing the step that was performed.
   986  		return deploy.Color(op) + display.getPreviewDoneText(step) + colors.Reset
   987  	}
   988  
   989  	getDescription := func() string {
   990  		opText := ""
   991  		if failed {
   992  			switch op {
   993  			case deploy.OpSame:
   994  				opText = "failed"
   995  			case deploy.OpCreate, deploy.OpCreateReplacement:
   996  				opText = "creating failed"
   997  			case deploy.OpUpdate:
   998  				opText = "updating failed"
   999  			case deploy.OpDelete, deploy.OpDeleteReplaced:
  1000  				opText = "deleting failed"
  1001  			case deploy.OpReplace:
  1002  				opText = "replacing failed"
  1003  			case deploy.OpRead, deploy.OpReadReplacement:
  1004  				opText = "reading failed"
  1005  			case deploy.OpRefresh:
  1006  				opText = "refreshing failed"
  1007  			case deploy.OpReadDiscard, deploy.OpDiscardReplaced:
  1008  				opText = "discarding failed"
  1009  			case deploy.OpImport, deploy.OpImportReplacement:
  1010  				opText = "importing failed"
  1011  			default:
  1012  				contract.Failf("Unrecognized resource step op: %v", op)
  1013  				return ""
  1014  			}
  1015  		} else {
  1016  			switch op {
  1017  			case deploy.OpSame:
  1018  				opText = ""
  1019  			case deploy.OpCreate:
  1020  				opText = "created"
  1021  			case deploy.OpUpdate:
  1022  				opText = "updated"
  1023  			case deploy.OpDelete:
  1024  				opText = "deleted"
  1025  			case deploy.OpReplace:
  1026  				opText = "replaced"
  1027  			case deploy.OpCreateReplacement:
  1028  				opText = "created replacement"
  1029  			case deploy.OpDeleteReplaced:
  1030  				opText = "deleted original"
  1031  			case deploy.OpRead:
  1032  				// nolint: goconst
  1033  				opText = "read"
  1034  			case deploy.OpReadReplacement:
  1035  				opText = "read for replacement"
  1036  			case deploy.OpRefresh:
  1037  				opText = "refresh"
  1038  			case deploy.OpReadDiscard:
  1039  				opText = "discarded"
  1040  			case deploy.OpDiscardReplaced:
  1041  				opText = "discarded original"
  1042  			case deploy.OpImport:
  1043  				opText = "imported"
  1044  			case deploy.OpImportReplacement:
  1045  				opText = "imported replacement"
  1046  			default:
  1047  				contract.Failf("Unrecognized resource step op: %v", op)
  1048  				return ""
  1049  			}
  1050  		}
  1051  		if op == deploy.OpSame || display.opts.deterministicOutput || display.opts.SuppressTimings {
  1052  			return opText
  1053  		}
  1054  
  1055  		start, ok := display.opStopwatch.start[step.URN]
  1056  		if !ok {
  1057  			return opText
  1058  		}
  1059  
  1060  		end, ok := display.opStopwatch.end[step.URN]
  1061  		if !ok {
  1062  			return opText
  1063  		}
  1064  
  1065  		opDuration := end.Sub(start).Seconds()
  1066  		if opDuration < 1 {
  1067  			// Display a more fine-grain duration as the operation
  1068  			// has completed.
  1069  			return fmt.Sprintf("%s (%.2fs)", opText, opDuration)
  1070  		}
  1071  		return fmt.Sprintf("%s (%ds)", opText, int(opDuration))
  1072  	}
  1073  
  1074  	if failed {
  1075  		return makeError(getDescription())
  1076  	}
  1077  
  1078  	return deploy.Color(op) + getDescription() + colors.Reset
  1079  }
  1080  
  1081  func (display *ProgressDisplay) getPreviewText(step engine.StepEventMetadata) string {
  1082  	switch step.Op {
  1083  	case deploy.OpSame:
  1084  		return ""
  1085  	case deploy.OpCreate:
  1086  		return "create"
  1087  	case deploy.OpUpdate:
  1088  		return "update"
  1089  	case deploy.OpDelete:
  1090  		return "delete"
  1091  	case deploy.OpReplace:
  1092  		return "replace"
  1093  	case deploy.OpCreateReplacement:
  1094  		return "create replacement"
  1095  	case deploy.OpDeleteReplaced:
  1096  		return "delete original"
  1097  	case deploy.OpRead:
  1098  		// nolint: goconst
  1099  		return "read"
  1100  	case deploy.OpReadReplacement:
  1101  		return "read for replacement"
  1102  	case deploy.OpRefresh:
  1103  		return "refreshing"
  1104  	case deploy.OpReadDiscard:
  1105  		return "discard"
  1106  	case deploy.OpDiscardReplaced:
  1107  		return "discard original"
  1108  	case deploy.OpImport:
  1109  		return "import"
  1110  	case deploy.OpImportReplacement:
  1111  		return "import replacement"
  1112  	}
  1113  
  1114  	contract.Failf("Unrecognized resource step op: %v", step.Op)
  1115  	return ""
  1116  }
  1117  
  1118  // getPreviewDoneText returns a textual representation for this step, suitable for display during a preview once the
  1119  // preview has completed.
  1120  func (display *ProgressDisplay) getPreviewDoneText(step engine.StepEventMetadata) string {
  1121  	switch step.Op {
  1122  	case deploy.OpSame:
  1123  		return ""
  1124  	case deploy.OpCreate:
  1125  		return "create"
  1126  	case deploy.OpUpdate:
  1127  		return "update"
  1128  	case deploy.OpDelete:
  1129  		return "delete"
  1130  	case deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced, deploy.OpReadReplacement,
  1131  		deploy.OpDiscardReplaced:
  1132  		return "replace"
  1133  	case deploy.OpRead:
  1134  		// nolint: goconst
  1135  		return "read"
  1136  	case deploy.OpRefresh:
  1137  		return "refresh"
  1138  	case deploy.OpReadDiscard:
  1139  		return "discard"
  1140  	case deploy.OpImport, deploy.OpImportReplacement:
  1141  		return "import"
  1142  	}
  1143  
  1144  	contract.Failf("Unrecognized resource step op: %v", step.Op)
  1145  	return ""
  1146  }
  1147  
  1148  func (display *ProgressDisplay) getStepOp(step engine.StepEventMetadata) display.StepOp {
  1149  	op := step.Op
  1150  
  1151  	// We will commonly hear about replacements as an actual series of steps.  i.e. 'create
  1152  	// replacement', 'replace', 'delete original'.  During the actual application of these steps we
  1153  	// want to see these individual steps.  However, both before we apply all of them, and after
  1154  	// they're all done, we want to show this as a single conceptual 'replace'/'replaced' step.
  1155  	//
  1156  	// Note: in non-interactive mode we can show these all as individual steps.  This only applies
  1157  	// to interactive mode, where there is only one line shown per resource, and we want it to be as
  1158  	// clear as possible
  1159  	if display.isTerminal {
  1160  		// During preview, show the steps for replacing as a single 'replace' plan.
  1161  		// Once done, show the steps for replacing as a single 'replaced' step.
  1162  		// During update, we'll show these individual steps.
  1163  		if display.isPreview || display.done {
  1164  			if op == deploy.OpCreateReplacement || op == deploy.OpDeleteReplaced || op == deploy.OpDiscardReplaced {
  1165  				return deploy.OpReplace
  1166  			}
  1167  		}
  1168  	}
  1169  
  1170  	return op
  1171  }
  1172  
  1173  func (display *ProgressDisplay) getStepOpLabel(step engine.StepEventMetadata, done bool) string {
  1174  	return deploy.Prefix(display.getStepOp(step), done) + colors.Reset
  1175  }
  1176  
  1177  func (display *ProgressDisplay) getStepInProgressDescription(step engine.StepEventMetadata) string {
  1178  	op := display.getStepOp(step)
  1179  
  1180  	if isRootStack(step) && op == deploy.OpSame {
  1181  		// most of the time a stack is unchanged.  in that case we just show it as "running->done".
  1182  		// otherwise, we show what is actually happening to it.
  1183  		return "running"
  1184  	}
  1185  
  1186  	getDescription := func() string {
  1187  		if display.isPreview {
  1188  			return display.getPreviewText(step)
  1189  		}
  1190  
  1191  		opText := ""
  1192  		switch op {
  1193  		case deploy.OpSame:
  1194  			opText = ""
  1195  		case deploy.OpCreate:
  1196  			opText = "creating"
  1197  		case deploy.OpUpdate:
  1198  			opText = "updating"
  1199  		case deploy.OpDelete:
  1200  			opText = "deleting"
  1201  		case deploy.OpReplace:
  1202  			opText = "replacing"
  1203  		case deploy.OpCreateReplacement:
  1204  			opText = "creating replacement"
  1205  		case deploy.OpDeleteReplaced:
  1206  			opText = "deleting original"
  1207  		case deploy.OpRead:
  1208  			opText = "reading"
  1209  		case deploy.OpReadReplacement:
  1210  			opText = "reading for replacement"
  1211  		case deploy.OpRefresh:
  1212  			opText = "refreshing"
  1213  		case deploy.OpReadDiscard:
  1214  			opText = "discarding"
  1215  		case deploy.OpDiscardReplaced:
  1216  			opText = "discarding original"
  1217  		case deploy.OpImport:
  1218  			opText = "importing"
  1219  		case deploy.OpImportReplacement:
  1220  			opText = "importing replacement"
  1221  		default:
  1222  			contract.Failf("Unrecognized resource step op: %v", op)
  1223  			return ""
  1224  		}
  1225  
  1226  		if op == deploy.OpSame || display.opts.deterministicOutput || display.opts.SuppressTimings {
  1227  			return opText
  1228  		}
  1229  
  1230  		// Calculate operation time elapsed.
  1231  		start, ok := display.opStopwatch.start[step.URN]
  1232  		if !ok {
  1233  			return opText
  1234  		}
  1235  
  1236  		secondsElapsed := time.Now().Sub(start).Seconds()
  1237  		return fmt.Sprintf("%s (%ds)", opText, int(secondsElapsed))
  1238  
  1239  	}
  1240  	return deploy.ColorProgress(op) + getDescription() + colors.Reset
  1241  }