github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/backend/display/rows.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  package display
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	"io"
    21  	"sort"
    22  	"strings"
    23  
    24  	"github.com/dustin/go-humanize/english"
    25  	"github.com/pulumi/pulumi/pkg/v3/engine"
    26  	"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
    27  	"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
    28  	"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
    29  	"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
    30  	"github.com/pulumi/pulumi/sdk/v3/go/common/display"
    31  	"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
    32  )
    33  
    34  type Row interface {
    35  	DisplayOrderIndex() int
    36  	SetDisplayOrderIndex(index int)
    37  
    38  	ColorizedColumns() []string
    39  	ColorizedSuffix() string
    40  
    41  	HideRowIfUnnecessary() bool
    42  	SetHideRowIfUnnecessary(value bool)
    43  }
    44  
    45  type ResourceRow interface {
    46  	Row
    47  
    48  	Step() engine.StepEventMetadata
    49  	SetStep(step engine.StepEventMetadata)
    50  	AddOutputStep(step engine.StepEventMetadata)
    51  
    52  	// The tick we were on when we created this row.  Purely used for generating an
    53  	// ellipses to show progress for in-flight resources.
    54  	Tick() int
    55  
    56  	IsDone() bool
    57  
    58  	SetFailed()
    59  
    60  	DiagInfo() *DiagInfo
    61  	PolicyPayloads() []engine.PolicyViolationEventPayload
    62  
    63  	RecordDiagEvent(diagEvent engine.Event)
    64  	RecordPolicyViolationEvent(diagEvent engine.Event)
    65  }
    66  
    67  // Implementation of a Row, used for the header of the grid.
    68  type headerRowData struct {
    69  	display *ProgressDisplay
    70  	columns []string
    71  }
    72  
    73  func (data *headerRowData) HideRowIfUnnecessary() bool {
    74  	return false
    75  }
    76  
    77  func (data *headerRowData) SetHideRowIfUnnecessary(value bool) {
    78  }
    79  
    80  func (data *headerRowData) DisplayOrderIndex() int {
    81  	// sort the header before all other rows
    82  	return -1
    83  }
    84  
    85  func (data *headerRowData) SetDisplayOrderIndex(time int) {
    86  	// Nothing to do here.   Header is always at the same index.
    87  }
    88  
    89  func (data *headerRowData) ColorizedColumns() []string {
    90  	if len(data.columns) == 0 {
    91  		header := func(msg string) string {
    92  			return columnHeader(msg)
    93  		}
    94  
    95  		var statusColumn string
    96  		if data.display.isPreview {
    97  			statusColumn = header("Plan")
    98  		} else {
    99  			statusColumn = header("Status")
   100  		}
   101  		data.columns = []string{"", header("Type"), header("Name"), statusColumn, header("Info")}
   102  	}
   103  
   104  	return data.columns
   105  }
   106  
   107  func (data *headerRowData) ColorizedSuffix() string {
   108  	return ""
   109  }
   110  
   111  // resourceRowData is the implementation of a row used for all the resource rows in the grid.
   112  type resourceRowData struct {
   113  	displayOrderIndex int
   114  
   115  	display *ProgressDisplay
   116  
   117  	// The change that the engine wants apply to that resource.
   118  	step        engine.StepEventMetadata
   119  	outputSteps []engine.StepEventMetadata
   120  
   121  	// The tick we were on when we created this row.  Purely used for generating an
   122  	// ellipses to show progress for in-flight resources.
   123  	tick int
   124  
   125  	// If we failed this operation for any reason.
   126  	failed bool
   127  
   128  	diagInfo       *DiagInfo
   129  	policyPayloads []engine.PolicyViolationEventPayload
   130  
   131  	// If this row should be hidden by default.  We will hide unless we have any child nodes
   132  	// we need to show.
   133  	hideRowIfUnnecessary bool
   134  }
   135  
   136  func (data *resourceRowData) DisplayOrderIndex() int {
   137  	// sort the header before all other rows
   138  	return data.displayOrderIndex
   139  }
   140  
   141  func (data *resourceRowData) SetDisplayOrderIndex(index int) {
   142  	// only set this if it's the first time.
   143  	if data.displayOrderIndex == 0 {
   144  		data.displayOrderIndex = index
   145  	}
   146  }
   147  
   148  func (data *resourceRowData) HideRowIfUnnecessary() bool {
   149  	return data.hideRowIfUnnecessary
   150  }
   151  
   152  func (data *resourceRowData) SetHideRowIfUnnecessary(value bool) {
   153  	data.hideRowIfUnnecessary = value
   154  }
   155  
   156  func (data *resourceRowData) Step() engine.StepEventMetadata {
   157  	return data.step
   158  }
   159  
   160  func (data *resourceRowData) SetStep(step engine.StepEventMetadata) {
   161  	data.step = step
   162  }
   163  
   164  func (data *resourceRowData) AddOutputStep(step engine.StepEventMetadata) {
   165  	data.outputSteps = append(data.outputSteps, step)
   166  }
   167  
   168  func (data *resourceRowData) Tick() int {
   169  	return data.tick
   170  }
   171  
   172  func (data *resourceRowData) Failed() bool {
   173  	return data.failed
   174  }
   175  
   176  func (data *resourceRowData) SetFailed() {
   177  	data.failed = true
   178  }
   179  
   180  func (data *resourceRowData) DiagInfo() *DiagInfo {
   181  	return data.diagInfo
   182  }
   183  
   184  func (data *resourceRowData) RecordDiagEvent(event engine.Event) {
   185  	payload := event.Payload().(engine.DiagEventPayload)
   186  	data.recordDiagEventPayload(payload)
   187  }
   188  
   189  func (data *resourceRowData) recordDiagEventPayload(payload engine.DiagEventPayload) {
   190  	diagInfo := data.diagInfo
   191  	diagInfo.LastDiag = &payload
   192  
   193  	if payload.Severity == diag.Error {
   194  		diagInfo.LastError = &payload
   195  	}
   196  
   197  	if diagInfo.StreamIDToDiagPayloads == nil {
   198  		diagInfo.StreamIDToDiagPayloads = make(map[int32][]engine.DiagEventPayload)
   199  	}
   200  
   201  	payloads := diagInfo.StreamIDToDiagPayloads[payload.StreamID]
   202  	payloads = append(payloads, payload)
   203  	diagInfo.StreamIDToDiagPayloads[payload.StreamID] = payloads
   204  
   205  	if !payload.Ephemeral {
   206  		switch payload.Severity {
   207  		case diag.Error:
   208  			diagInfo.ErrorCount++
   209  		case diag.Warning:
   210  			diagInfo.WarningCount++
   211  		case diag.Infoerr:
   212  			diagInfo.InfoCount++
   213  		case diag.Info:
   214  			diagInfo.InfoCount++
   215  		case diag.Debug:
   216  			diagInfo.DebugCount++
   217  		}
   218  	}
   219  }
   220  
   221  // PolicyInfo returns the PolicyInfo object associated with the resourceRowData.
   222  func (data *resourceRowData) PolicyPayloads() []engine.PolicyViolationEventPayload {
   223  	return data.policyPayloads
   224  }
   225  
   226  // RecordPolicyViolationEvent records a policy event with the resourceRowData.
   227  func (data *resourceRowData) RecordPolicyViolationEvent(event engine.Event) {
   228  	pePayload := event.Payload().(engine.PolicyViolationEventPayload)
   229  	data.policyPayloads = append(data.policyPayloads, pePayload)
   230  }
   231  
   232  type column int
   233  
   234  const (
   235  	opColumn     column = 0
   236  	typeColumn   column = 1
   237  	nameColumn   column = 2
   238  	statusColumn column = 3
   239  	infoColumn   column = 4
   240  )
   241  
   242  func (data *resourceRowData) IsDone() bool {
   243  	if data.failed {
   244  		// consider a failed resource 'done'.
   245  		return true
   246  	}
   247  
   248  	if data.display.done {
   249  		// if the display is done, then we're definitely done.
   250  		return true
   251  	}
   252  
   253  	if isRootStack(data.step) {
   254  		// the root stack only becomes 'done' once the program has completed (i.e. the condition
   255  		// checked just above this).  If the program is not finished, then always show the root
   256  		// stack as not done so the user sees "running..." presented for it.
   257  		return false
   258  	}
   259  
   260  	// We're done if we have the output-step for whatever step operation we're performing
   261  	return data.ContainsOutputsStep(data.step.Op)
   262  }
   263  
   264  func (data *resourceRowData) ContainsOutputsStep(op display.StepOp) bool {
   265  	for _, s := range data.outputSteps {
   266  		if s.Op == op {
   267  			return true
   268  		}
   269  	}
   270  
   271  	return false
   272  }
   273  
   274  func (data *resourceRowData) ColorizedSuffix() string {
   275  	if !data.IsDone() && data.display.isTerminal {
   276  		op := data.display.getStepOp(data.step)
   277  		if op != deploy.OpSame || isRootURN(data.step.URN) {
   278  			suffixes := data.display.suffixesArray
   279  			ellipses := suffixes[(data.tick+data.display.currentTick)%len(suffixes)]
   280  
   281  			return deploy.ColorProgress(op) + ellipses + colors.Reset
   282  		}
   283  	}
   284  
   285  	return ""
   286  }
   287  
   288  func (data *resourceRowData) ColorizedColumns() []string {
   289  	step := data.step
   290  
   291  	urn := data.step.URN
   292  	if urn == "" {
   293  		// If we don't have a URN yet, mock parent it to the global stack.
   294  		urn = resource.DefaultRootStackURN(data.display.stack.Q(), data.display.proj)
   295  	}
   296  	name := string(urn.Name())
   297  	typ := simplifyTypeName(urn.Type())
   298  
   299  	done := data.IsDone()
   300  
   301  	columns := make([]string, 5)
   302  	columns[opColumn] = data.display.getStepOpLabel(step, done)
   303  	columns[typeColumn] = typ
   304  	columns[nameColumn] = name
   305  
   306  	diagInfo := data.diagInfo
   307  
   308  	if done {
   309  		failed := data.failed || diagInfo.ErrorCount > 0
   310  		columns[statusColumn] = data.display.getStepDoneDescription(step, failed)
   311  	} else {
   312  		columns[statusColumn] = data.display.getStepInProgressDescription(step)
   313  	}
   314  
   315  	columns[infoColumn] = data.getInfoColumn()
   316  	return columns
   317  }
   318  
   319  func (data *resourceRowData) getInfoColumn() string {
   320  	step := data.step
   321  	switch step.Op {
   322  	case deploy.OpCreateReplacement, deploy.OpDeleteReplaced:
   323  		// if we're doing a replacement, see if we can find a replace step that contains useful
   324  		// information to display.
   325  		for _, outputStep := range data.outputSteps {
   326  			if outputStep.Op == deploy.OpReplace {
   327  				step = outputStep
   328  			}
   329  		}
   330  
   331  	case deploy.OpImport, deploy.OpImportReplacement:
   332  		// If we're doing an import, see if we have the imported state to diff.
   333  		for _, outputStep := range data.outputSteps {
   334  			if outputStep.Op == step.Op {
   335  				step = outputStep
   336  			}
   337  		}
   338  	}
   339  
   340  	var diagMsg string
   341  	appendDiagMessage := func(msg string) {
   342  		if diagMsg != "" {
   343  			diagMsg += "; "
   344  		}
   345  
   346  		diagMsg += msg
   347  	}
   348  
   349  	changes := getDiffInfo(step, data.display.action)
   350  	if colors.Never.Colorize(changes) != "" {
   351  		appendDiagMessage("[" + changes + "]")
   352  	}
   353  
   354  	diagInfo := data.diagInfo
   355  	if data.display.done {
   356  		// If we are done, show a summary of how many messages were printed.
   357  		if c := diagInfo.ErrorCount; c > 0 {
   358  			appendDiagMessage(fmt.Sprintf("%d %s%s%s",
   359  				c, colors.SpecError, english.PluralWord(c, "error", ""), colors.Reset))
   360  		}
   361  		if c := diagInfo.WarningCount; c > 0 {
   362  			appendDiagMessage(fmt.Sprintf("%d %s%s%s",
   363  				c, colors.SpecWarning, english.PluralWord(c, "warning", ""), colors.Reset))
   364  		}
   365  		if c := diagInfo.InfoCount; c > 0 {
   366  			appendDiagMessage(fmt.Sprintf("%d %s%s%s",
   367  				c, colors.SpecInfo, english.PluralWord(c, "message", ""), colors.Reset))
   368  		}
   369  		if c := diagInfo.DebugCount; c > 0 {
   370  			appendDiagMessage(fmt.Sprintf("%d %s%s%s",
   371  				c, colors.SpecDebug, english.PluralWord(c, "debug", ""), colors.Reset))
   372  		}
   373  	} else {
   374  		// If we're not totally done, and we're in the tree-view, just print out the last error (if
   375  		// there is one) next to the status message. This is helpful for long running tasks to know
   376  		// something bad has happened. However, once done, we print the diagnostics at the bottom, so we don't
   377  		// need to show this.
   378  		//
   379  		// if we're not in the tree-view (i.e. non-interactive mode), then we want to print out
   380  		// whatever the last diagnostics was that we got.  This way, as we're hearing about
   381  		// diagnostic events, we're always printing out the last one.
   382  
   383  		diagnostic := data.diagInfo.LastDiag
   384  		if data.display.isTerminal && data.diagInfo.LastError != nil {
   385  			diagnostic = data.diagInfo.LastError
   386  		}
   387  
   388  		if diagnostic != nil {
   389  			eventMsg := data.display.renderProgressDiagEvent(*diagnostic, true /*includePrefix:*/)
   390  			if eventMsg != "" {
   391  				appendDiagMessage(eventMsg)
   392  			}
   393  		}
   394  	}
   395  
   396  	newLineIndex := strings.Index(diagMsg, "\n")
   397  	if newLineIndex >= 0 {
   398  		diagMsg = diagMsg[0:newLineIndex]
   399  	}
   400  
   401  	return diagMsg
   402  }
   403  
   404  func getDiffInfo(step engine.StepEventMetadata, action apitype.UpdateKind) string {
   405  	diffOutputs := action == apitype.RefreshUpdate
   406  	changesBuf := &bytes.Buffer{}
   407  	if step.Old != nil && step.New != nil {
   408  		var diff *resource.ObjectDiff
   409  		if step.DetailedDiff != nil {
   410  			diff = engine.TranslateDetailedDiff(&step)
   411  		} else if diffOutputs {
   412  			if step.Old.Outputs != nil && step.New.Outputs != nil {
   413  				diff = step.Old.Outputs.Diff(step.New.Outputs)
   414  			}
   415  		} else if step.Old.Inputs != nil && step.New.Inputs != nil {
   416  			diff = step.Old.Inputs.Diff(step.New.Inputs)
   417  		}
   418  
   419  		// Show a diff if either `provider` or `protect` changed; they might not show a diff via inputs or outputs, but
   420  		// it is still useful to show that these changed in output.
   421  		recordMetadataDiff := func(name string, old, new resource.PropertyValue) {
   422  			if old != new {
   423  				if diff == nil {
   424  					diff = &resource.ObjectDiff{
   425  						Adds:    make(resource.PropertyMap),
   426  						Deletes: make(resource.PropertyMap),
   427  						Sames:   make(resource.PropertyMap),
   428  						Updates: make(map[resource.PropertyKey]resource.ValueDiff),
   429  					}
   430  				}
   431  
   432  				diff.Updates[resource.PropertyKey(name)] = resource.ValueDiff{Old: old, New: new}
   433  			}
   434  		}
   435  
   436  		recordMetadataDiff("provider",
   437  			resource.NewStringProperty(step.Old.Provider), resource.NewStringProperty(step.New.Provider))
   438  		recordMetadataDiff("protect",
   439  			resource.NewBoolProperty(step.Old.Protect), resource.NewBoolProperty(step.New.Protect))
   440  
   441  		if diff != nil {
   442  			writeString(changesBuf, "diff: ")
   443  
   444  			updates := make(resource.PropertyMap)
   445  			for k := range diff.Updates {
   446  				updates[k] = resource.PropertyValue{}
   447  			}
   448  
   449  			filteredKeys := func(m resource.PropertyMap) []string {
   450  				keys := make([]string, 0, len(m))
   451  				for k := range m {
   452  					keys = append(keys, string(k))
   453  				}
   454  				return keys
   455  			}
   456  			if include := step.Diffs; include != nil {
   457  				includeSet := make(map[resource.PropertyKey]bool)
   458  				for _, k := range include {
   459  					includeSet[k] = true
   460  				}
   461  				filteredKeys = func(m resource.PropertyMap) []string {
   462  					var filteredKeys []string
   463  					for k := range m {
   464  						if includeSet[k] {
   465  							filteredKeys = append(filteredKeys, string(k))
   466  						}
   467  					}
   468  					return filteredKeys
   469  				}
   470  			}
   471  
   472  			writePropertyKeys(changesBuf, filteredKeys(diff.Adds), deploy.OpCreate)
   473  			writePropertyKeys(changesBuf, filteredKeys(diff.Deletes), deploy.OpDelete)
   474  			writePropertyKeys(changesBuf, filteredKeys(updates), deploy.OpUpdate)
   475  		}
   476  	}
   477  
   478  	fprintIgnoreError(changesBuf, colors.Reset)
   479  	return changesBuf.String()
   480  }
   481  
   482  func writePropertyKeys(b io.StringWriter, keys []string, op display.StepOp) {
   483  	if len(keys) > 0 {
   484  		writeString(b, strings.Trim(deploy.Prefix(op, true /*done*/), " "))
   485  
   486  		sort.Strings(keys)
   487  
   488  		for index, k := range keys {
   489  			if index != 0 {
   490  				writeString(b, ",")
   491  			}
   492  			writeString(b, k)
   493  		}
   494  
   495  		writeString(b, colors.Reset)
   496  	}
   497  }