Switch to a new grid-view for the progress display. (#1201)

This commit is contained in:
CyrusNajmabadi 2018-04-15 12:47:53 -07:00 committed by GitHub
parent 246aaba00e
commit 541b8b3f4e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 518 additions and 261 deletions

View file

@ -165,6 +165,7 @@ func renderSummaryEvent(event engine.SummaryEventPayload, opts backend.DisplayOp
}
}
}
if c := changes[deploy.OpSame]; c > 0 {
fprintfIgnoreError(out, " %v %v unchanged\n", c, plural("resource", c))
}

View file

@ -30,24 +30,12 @@ import (
"golang.org/x/crypto/ssh/terminal"
)
// Status helps us keep track for a resource as it is worked on by the engine.
type Status struct {
// The simple short ID we have generated for the resource to present it to the user.
// Usually similar to the form: aws.Function("name")
ID string
type DiagInfo struct {
ErrorCount, WarningCount, InfoCount, DebugCount int
// The change that the engine wants apply to that resource.
Step engine.StepEventMetadata
// The tick we were on when we created this status. Purely used for generating an
// ellipses to show progress for in-flight resources.
Tick int
// If the engine finished processing this resources.
Done bool
// If we failed this operation for any reason.
Failed bool
// The last event of each severity kind. We'll print out the most significant of these next
// to a resource while it is in progress.
LastError, LastWarning, LastInfoError, LastInfo, LastDebug *engine.Event
// All the diagnostic events we've heard about this resource. We'll print the last
// diagnostic in the status region while a resource is in progress. At the end we'll
@ -58,12 +46,12 @@ type Status struct {
var (
// simple regex to take our names like "aws:function:Function" and convert to
// "aws:Function"
typeNameRegex = regexp.MustCompile("^(.*):(.*):(.*)$")
typeNameRegex = regexp.MustCompile("^(.*):(.*)/(.*):(.*)$")
)
func simplifyTypeName(typ tokens.Type) string {
typeString := string(typ)
return typeNameRegex.ReplaceAllString(typeString, "$1:$3")
return typeNameRegex.ReplaceAllString(typeString, "$1:$2:$4")
}
// getEventUrn returns the resource URN associated with an event, or the empty URN if this is not an
@ -110,55 +98,6 @@ func (display *ProgressDisplay) writeBlankLine() {
display.writeSimpleMessage(" ")
}
// Returns the worst diagnostic we've seen, along with counts of all the diagnostic kinds. Used to
// produce a diagnostic string to go along with any resource if it has had any issues.
func getDiagnosticInformation(status Status) (
worstDiag *engine.Event, errorEvents, warningEvents, infoEvents, debugEvents int) {
errors := 0
warnings := 0
infos := 0
debugs := 0
var lastError, lastInfoError, lastWarning, lastInfo, lastDebug *engine.Event
for _, ev := range status.DiagEvents {
payload := ev.Payload.(engine.DiagEventPayload)
switch payload.Severity {
case diag.Error:
errors++
lastError = &ev
case diag.Warning:
warnings++
lastWarning = &ev
case diag.Infoerr:
infos++
lastInfoError = &ev
case diag.Info:
infos++
lastInfo = &ev
case diag.Debug:
debugs++
lastDebug = &ev
}
}
if lastError != nil {
worstDiag = lastError
} else if lastWarning != nil {
worstDiag = lastWarning
} else if lastInfoError != nil {
worstDiag = lastInfoError
} else if lastInfo != nil {
worstDiag = lastInfo
} else {
worstDiag = lastDebug
}
return worstDiag, errors, warnings, infos, debugs
}
type ProgressDisplay struct {
opts backend.DisplayOptions
progressOutput progress.Output
@ -181,14 +120,15 @@ type ProgressDisplay struct {
// The length of the largest ID we've seen. We use this so we can align status messages per
// resource. i.e. status messages for shorter IDs will get passed with spaces so that
// everything aligns.
maxIDLength int
maxColumnLengths []int
// What tick we're currently on. Used to determine the number of ellipses to concat to
// a status message to help indicate that things are still working.
currentTick int
rows []Row
// A mapping from each resource URN we are told about to its current status.
eventUrnToStatus map[resource.URN]Status
eventUrnToResourceRow map[resource.URN]ResourceRow
// Remember if we're a terminal or not. In a terminal we get a little bit fancier.
// For example, we'll go back and update previous status messages to make sure things
@ -200,7 +140,6 @@ type ProgressDisplay struct {
// Maps used so we can generate short IDs for resource urns.
urnToID map[resource.URN]string
idToUrn map[string]resource.URN
// If all progress messages are done and we can print out the final display.
Done bool
@ -225,11 +164,10 @@ func DisplayProgressEvents(
progressOutput := streamformatter.NewJSONStreamFormatter().NewProgressOutput(pipeWriter, false)
display := &ProgressDisplay{
opts: opts,
progressOutput: progressOutput,
eventUrnToStatus: make(map[resource.URN]Status),
urnToID: make(map[resource.URN]string),
idToUrn: make(map[string]resource.URN),
opts: opts,
progressOutput: progressOutput,
eventUrnToResourceRow: make(map[resource.URN]ResourceRow),
urnToID: make(map[resource.URN]string),
}
display.initializeTermInfo()
@ -267,49 +205,24 @@ func (display *ProgressDisplay) initializeTermInfo() {
display.terminalWidth = terminalWidth
}
func (display *ProgressDisplay) makeID(urn resource.URN) string {
makeSingleID := func(suffix int) string {
var id string
if urn == "" {
id = "global"
} else {
id = simplifyTypeName(urn.Type()) + "(\"" + string(urn.Name()) + "\")"
}
if suffix > 0 {
id += fmt.Sprintf("-%v", suffix)
}
return id
}
if id, has := display.urnToID[urn]; !has {
for i := 0; ; i++ {
id = makeSingleID(i)
if _, has = display.idToUrn[id]; !has {
display.urnToID[urn] = id
display.idToUrn[id] = urn
return id
}
}
} else {
return id
}
}
// Gets the padding necessary to prepend to a message in order to keep it aligned in the
// terminal.
func (display *ProgressDisplay) getMessagePadding(status Status) string {
extraWhitespace := 0
func (display *ProgressDisplay) getMessagePadding(uncolorizedColumns []string, columnIndex int) string {
extraWhitespace := 1
// In the terminal we try to align the status messages for each resource.
// do not bother with this in the non-terminal case.
if display.isTerminal {
id := status.ID
extraWhitespace = display.maxIDLength - len(id)
contract.Assertf(extraWhitespace >= 0, "Neg whitespace. %v %s", display.maxIDLength, id)
column := uncolorizedColumns[columnIndex]
maxIDLength := display.maxColumnLengths[columnIndex]
extraWhitespace = maxIDLength - len(column)
contract.Assertf(extraWhitespace >= 0, "Neg whitespace. %v %s", maxIDLength, column)
if columnIndex > 0 {
// docker puts an extra whitespace one the first column. We replicate that for all
// other columns
extraWhitespace++
}
}
return strings.Repeat(" ", extraWhitespace)
@ -319,99 +232,105 @@ func (display *ProgressDisplay) getMessagePadding(status Status) string {
// status, then some amount of optional padding, then some amount of msgWithColors, then the
// suffix. Importantly, if there isn't enough room to display all of that on the terminal, then
// the msg will be truncated to try to make it fit.
func (display *ProgressDisplay) getPaddedMessage(status Status, msgWithColors string, suffix string) string {
id := status.ID
padding := display.getMessagePadding(status)
func (display *ProgressDisplay) getPaddedMessage(
colorizedColumns, uncolorizedColumns []string,
colorizedSuffix, uncolorizedSuffix string) string {
colorizedMessage := ""
// Figure out the last column that is non-empty. That's where we'll add the suffix to.
lastNonEmptyColumn := len(uncolorizedColumns)
for lastNonEmptyColumn > 0 && uncolorizedColumns[lastNonEmptyColumn-1] == "" {
lastNonEmptyColumn--
}
for i := 1; i < lastNonEmptyColumn; i++ {
padding := display.getMessagePadding(uncolorizedColumns, i-1)
colorizedColumn := padding + colorizedColumns[i]
colorizedMessage += colorizedColumn
}
// In the terminal, only include the first line of the message
if display.isTerminal {
newLineIndex := strings.Index(msgWithColors, "\n")
newLineIndex := strings.Index(colorizedMessage, "\n")
if newLineIndex >= 0 {
msgWithColors = msgWithColors[0:newLineIndex]
colorizedMessage = colorizedMessage[0:newLineIndex]
}
// Ensure we don't go past the end of the terminal. Note: this is made complex due to
// msgWithColors having the color code information embedded with it. So we need to get
// the right substring of it, assuming that embedded colors are just markup and do not
// actually contribute to the length
maxMsgLength := display.terminalWidth - len(id) - len(":") - len(padding) - len(suffix) - 1
id := uncolorizedColumns[0]
maxMsgLength := display.terminalWidth - len(id) - len(": ") - len(uncolorizedSuffix) - 1
if maxMsgLength < 0 {
maxMsgLength = 0
}
msgWithColors = colors.TrimColorizedString(msgWithColors, maxMsgLength)
colorizedMessage = colors.TrimColorizedString(colorizedMessage, maxMsgLength)
}
return padding + msgWithColors + suffix
return colorizedMessage + colorizedSuffix
}
// Gets the single line summary to show for a resource. This will include the current state of
// the resource (i.e. "Creating", "Replaced", "Failed", etc.) as well as relevant diagnostic
// information if there is any.
func (display *ProgressDisplay) getUnpaddedStatusSummary(status Status) string {
if status.Step.Op == "" {
contract.Failf("Finishing a resource we never heard about: '%s'", status.ID)
}
func (display *ProgressDisplay) refreshSingleRow(row Row) {
colorizedColumns := row.ColorizedColumns()
uncolorizedColumns := row.UncolorizedColumns()
colorizedSuffix := row.ColorizedSuffix()
uncolorizedSuffix := colors.Never.Colorize(colorizedSuffix)
worstDiag, errors, warnings, infos, debugs := getDiagnosticInformation(status)
failed := status.Failed || errors > 0
msg := display.getMetadataSummary(status.Step, status.Done, failed)
if errors > 0 {
msg += fmt.Sprintf(", %v error(s)", errors)
}
if warnings > 0 {
msg += fmt.Sprintf(", %v warning(s)", warnings)
}
if infos > 0 {
msg += fmt.Sprintf(", %v info message(s)", infos)
}
if debugs > 0 {
msg += fmt.Sprintf(", %v debug message(s)", debugs)
}
// If we're not totally done, also print out the worst diagnostic next to the status message.
// This is helpful for long running tasks to know what's going on. However, once done, we print
// the diagnostics at the bottom, so we don't need to show this.
if worstDiag != nil && !display.Done {
diagMsg := display.renderProgressDiagEvent(*worstDiag)
if diagMsg != "" {
msg += ". " + diagMsg
}
}
return msg
}
var ellipsesArray = []string{"", ".", "..", "..."}
func (display *ProgressDisplay) refreshSingleStatusMessage(status Status) {
unpaddedMsg := display.getUnpaddedStatusSummary(status)
suffix := ""
if !status.Done {
suffix = ellipsesArray[(status.Tick+display.currentTick)%len(ellipsesArray)]
}
msg := display.getPaddedMessage(status, unpaddedMsg, suffix)
msg := display.getPaddedMessage(
colorizedColumns, uncolorizedColumns, colorizedSuffix, uncolorizedSuffix)
display.colorizeAndWriteProgress(progress.Progress{
ID: status.ID,
ID: display.opts.Color.Colorize(colorizedColumns[0]),
Action: msg,
})
}
func (display *ProgressDisplay) refreshAllStatusMessages(includeDone bool) {
for _, v := range display.eventUrnToStatus {
if v.Done && !includeDone {
continue
// Ensure our stored dimension info is up to date. Returns 'true' if the stored dimension info is
// updated.
func (display *ProgressDisplay) updateDimensions() bool {
updated := false
// don't do any refreshing if we're not in a terminal
if display.isTerminal {
currentTerminalWidth, _, err := terminal.GetSize(int(os.Stdout.Fd()))
contract.IgnoreError(err)
if currentTerminalWidth != display.terminalWidth {
// terminal width changed. Refresh everything
display.terminalWidth = currentTerminalWidth
updated = true
}
display.refreshSingleStatusMessage(v)
for _, row := range display.rows {
columns := row.UncolorizedColumns()
if len(display.maxColumnLengths) == 0 {
display.maxColumnLengths = make([]int, len(columns))
}
for i, column := range columns {
if len(column) > display.maxColumnLengths[i] {
display.maxColumnLengths[i] = len(column)
updated = true
}
}
}
}
return updated
}
func (display *ProgressDisplay) refreshAllRowsIfInTerminal() {
if display.isTerminal {
// make sure our stored dimension info is up to date
display.updateDimensions()
for _, row := range display.rows {
display.refreshSingleRow(row)
}
}
}
@ -421,25 +340,37 @@ func (display *ProgressDisplay) refreshAllStatusMessages(includeDone bool) {
func (display *ProgressDisplay) processEndSteps() {
display.Done = true
// Mark all in progress resources as done.
for k, v := range display.eventUrnToStatus {
if !v.Done {
v.Done = true
display.eventUrnToStatus[k] = v
for _, v := range display.eventUrnToResourceRow {
// transition everything to the done state. If we're not in an a terminal and this is a
// transition, then print out the transition. Don't bother doing this in a terminal as
// we're going to refresh everything when we break out of the loop.
if !v.Done() {
v.SetDone()
if !display.isTerminal {
display.refreshSingleRow(v)
}
} else {
// Explicitly transition the status so that we clear out any cached data for it.
v.SetDone()
}
}
// Make sure we refresh everything. That way we remove any diagnostics printed on the line.
display.refreshAllStatusMessages(true /*includeDone*/)
// Now refresh everything. this ensures that we go back and remove things like the diagnostic
// messages from a status message (since we're going to print them all) below. Note, this will
// only do something in a terminal. This i what we want, because if we're not in a terminal we
// don't really want to reprint any finished items we've already printed.
display.refreshAllRowsIfInTerminal()
// Print all diagnostics we've seen.
wroteDiagnosticHeader := false
for _, status := range display.eventUrnToStatus {
if len(status.DiagEvents) > 0 {
for _, row := range display.eventUrnToResourceRow {
events := row.DiagInfo().DiagEvents
if len(events) > 0 {
wroteResourceHeader := false
for _, v := range status.DiagEvents {
for _, v := range events {
msg := display.renderProgressDiagEvent(v)
lines := strings.Split(msg, "\n")
@ -466,7 +397,11 @@ func (display *ProgressDisplay) processEndSteps() {
if !wroteResourceHeader {
wroteResourceHeader = true
display.writeSimpleMessage(" " + status.ID + ":")
columns := row.ColorizedColumns()
display.writeSimpleMessage(" " +
columns[idColumn] + ": " +
columns[typeColumn] + ": " +
columns[nameColumn])
}
for _, line := range lines {
@ -489,17 +424,11 @@ func (display *ProgressDisplay) processEndSteps() {
}
func (display *ProgressDisplay) processTick() {
// Got a tick. Update all the in-progress resources.
// Got a tick. Update all resources if we're in a terminal. If we're not, then this won't do
// anything.
display.currentTick++
currentTerminalWidth, _, _ := terminal.GetSize(int(os.Stdout.Fd()))
if currentTerminalWidth != display.terminalWidth {
// terminal width changed. Update our output.
display.terminalWidth = currentTerminalWidth
display.refreshAllStatusMessages(true /*includeDone*/)
} else {
display.refreshAllStatusMessages(false /*includeDone*/)
}
display.refreshAllRowsIfInTerminal()
}
func (display *ProgressDisplay) processNormalEvent(event engine.Event) {
@ -543,57 +472,60 @@ func (display *ProgressDisplay) processNormalEvent(event engine.Event) {
return
}
if len(display.rows) == 0 {
// about to make our first status message. make sure we present the header line first.
display.rows = append(display.rows, &headerRowData{})
}
// At this point, all events should relate to resources.
refreshAllStatuses := false
status, has := display.eventUrnToStatus[eventUrn]
row, has := display.eventUrnToResourceRow[eventUrn]
if !has {
id := fmt.Sprintf("%v", len(display.eventUrnToResourceRow)+1)
// first time we're hearing about this resource. Create an initial nearly-empty
// status for it, assigning it a nice short ID.
status = Status{Tick: display.currentTick}
status.Step.Op = deploy.OpSame
status.ID = display.makeID(eventUrn)
if display.isTerminal {
// in the terminal we want to align the status portions of messages. If we
// heard about a resource with a longer id, go and update all in-flight and
// finished resources so that their statuses get aligned.
if len(status.ID) > display.maxIDLength {
display.maxIDLength = len(status.ID)
refreshAllStatuses = true
}
row = &resourceRowData{
display: display,
id: id,
tick: display.currentTick,
diagInfo: &DiagInfo{},
step: engine.StepEventMetadata{Op: deploy.OpSame},
}
display.eventUrnToResourceRow[eventUrn] = row
display.rows = append(display.rows, row)
}
if event.Type == engine.ResourcePreEvent {
status.Step = event.Payload.(engine.ResourcePreEventPayload).Metadata
if status.Step.Op == "" {
step := event.Payload.(engine.ResourcePreEventPayload).Metadata
if step.Op == "" {
contract.Failf("Got empty op for %s", event.Type)
}
row.SetStep(step)
} else if event.Type == engine.ResourceOutputsEvent {
// transition the status to done.
if !isRootURN(eventUrn) {
status.Done = true
row.SetDone()
}
} else if event.Type == engine.ResourceOperationFailed {
status.Done = true
status.Failed = true
row.SetDone()
row.SetFailed()
} else if event.Type == engine.DiagEvent {
// also record this diagnostic so we print it at the end.
status.DiagEvents = append(status.DiagEvents, event)
row.RecordDiagEvent(event)
} else {
contract.Failf("Unhandled event type '%s'", event.Type)
}
// Ensure that this updated status is recorded.
display.eventUrnToStatus[eventUrn] = status
// refresh the progress information for this resource. (or update all resources if
// we need to realign everything)
if refreshAllStatuses {
display.refreshAllStatusMessages(true /*includeDone*/)
// See if this new status information causes us to have to refresh everything. Otherwise,
// just refresh the info for that single status message.
if display.updateDimensions() {
contract.Assertf(display.isTerminal, "we should only need to refresh if we're in a terminal")
display.refreshAllRowsIfInTerminal()
} else {
display.refreshSingleStatusMessage(status)
display.refreshSingleRow(row)
}
}
@ -601,6 +533,7 @@ func (display *ProgressDisplay) processEvents(ticker *time.Ticker, events <-chan
// Main processing loop. The purpose of this func is to read in events from the engine
// and translate them into Status objects and progress messages to be presented to the
// command line.
for {
select {
case <-ticker.C:
@ -629,40 +562,6 @@ func (display *ProgressDisplay) renderProgressDiagEvent(event engine.Event) stri
return strings.TrimRightFunc(payload.Message, unicode.IsSpace)
}
func (display *ProgressDisplay) getMetadataSummary(
step engine.StepEventMetadata, done bool, failed bool) string {
out := &bytes.Buffer{}
if done {
writeString(out, display.getStepDoneDescription(step, failed))
} else {
writeString(out, display.getStepInProgressDescription(step))
}
writeString(out, colors.Reset)
if step.Old != nil && step.New != nil && step.Old.Inputs != nil && step.New.Inputs != nil {
diff := step.Old.Inputs.Diff(step.New.Inputs)
if diff != nil {
writeString(out, " changes:")
updates := make(resource.PropertyMap)
for k := range diff.Updates {
updates[k] = resource.PropertyValue{}
}
writePropertyKeys(out, diff.Adds, deploy.OpCreate)
writePropertyKeys(out, diff.Deletes, deploy.OpDelete)
writePropertyKeys(out, updates, deploy.OpReplace)
}
}
fprintIgnoreError(out, colors.Reset)
return out.String()
}
func (display *ProgressDisplay) getStepDoneDescription(step engine.StepEventMetadata, failed bool) string {
makeError := func(v string) string {
return colors.SpecError + "**" + v + "**" + colors.Reset

357
pkg/backend/local/rows.go Normal file
View file

@ -0,0 +1,357 @@
// Copyright 2016-2018, Pulumi Corporation. All rights reserved.
package local
import (
"bytes"
"fmt"
"github.com/pulumi/pulumi/pkg/diag"
"github.com/pulumi/pulumi/pkg/diag/colors"
"github.com/pulumi/pulumi/pkg/engine"
"github.com/pulumi/pulumi/pkg/resource"
"github.com/pulumi/pulumi/pkg/resource/deploy"
"github.com/pulumi/pulumi/pkg/util/contract"
)
type Row interface {
ColorizedColumns() []string
UncolorizedColumns() []string
ColorizedSuffix() string
}
type ResourceRow interface {
Row
// The change that the engine wants apply to that resource.
SetStep(step engine.StepEventMetadata)
// The tick we were on when we created this row. Purely used for generating an
// ellipses to show progress for in-flight resources.
Tick() int
Done() bool
SetDone()
SetFailed()
DiagInfo() *DiagInfo
RecordDiagEvent(diagEvent engine.Event)
}
// Implementation of a Row, used for the header of the grid.
type headerRowData struct {
columns []string
uncolorizedColumns []string
}
func (data *headerRowData) ColorizedColumns() []string {
if len(data.columns) == 0 {
blue := func(msg string) string {
return colors.Blue + msg + colors.Reset
}
header := func(msg string) string {
return blue(msg)
}
data.columns = []string{"#", header("Resource Type"), header("Name"), header("Status"), header("Extra Info")}
}
return data.columns
}
func (data *headerRowData) UncolorizedColumns() []string {
if len(data.uncolorizedColumns) == 0 {
data.uncolorizedColumns = uncolorise(data.ColorizedColumns())
}
return data.uncolorizedColumns
}
func (data *headerRowData) ColorizedSuffix() string {
return ""
}
// Implementation of a row used for all the resource rows in the grid.
type resourceRowData struct {
display *ProgressDisplay
// The simple short ID we have generated for the resource to present it to the user.
// Usually similar to the form: aws.Function("name")
id string
// The change that the engine wants apply to that resource.
step engine.StepEventMetadata
// The tick we were on when we created this row. Purely used for generating an
// ellipses to show progress for in-flight resources.
tick int
// If the engine finished processing this resources.
done bool
// If we failed this operation for any reason.
failed bool
diagInfo *DiagInfo
columns []string
uncolorizedColumns []string
}
func (data *resourceRowData) SetStep(step engine.StepEventMetadata) {
data.step = step
data.ClearCachedData()
}
func (data *resourceRowData) Tick() int {
return data.tick
}
func (data *resourceRowData) Done() bool {
return data.done
}
func (data *resourceRowData) SetDone() {
data.done = true
data.ClearCachedData()
}
func (data *resourceRowData) Failed() bool {
return data.failed
}
func (data *resourceRowData) SetFailed() {
data.failed = true
data.ClearCachedData()
}
func (data *resourceRowData) DiagInfo() *DiagInfo {
return data.diagInfo
}
func (data *resourceRowData) RecordDiagEvent(event engine.Event) {
data.ClearCachedData()
diagInfo := data.diagInfo
payload := event.Payload.(engine.DiagEventPayload)
switch payload.Severity {
case diag.Error:
diagInfo.ErrorCount++
diagInfo.LastError = &event
case diag.Warning:
diagInfo.WarningCount++
diagInfo.LastWarning = &event
case diag.Infoerr:
diagInfo.InfoCount++
diagInfo.LastInfoError = &event
case diag.Info:
diagInfo.InfoCount++
diagInfo.LastInfo = &event
case diag.Debug:
diagInfo.DebugCount++
diagInfo.LastDebug = &event
}
diagInfo.DiagEvents = append(diagInfo.DiagEvents, event)
}
func (data *resourceRowData) ClearCachedData() {
data.columns = []string{}
data.uncolorizedColumns = []string{}
}
type column int
const (
idColumn column = 0
typeColumn column = 1
nameColumn column = 2
statusColumn column = 3
infoColumn column = 4
)
var ellipsesArray = []string{"", ".", "..", "..."}
func (data *resourceRowData) ColorizedSuffix() string {
if !data.done {
uncolorizedColumns := data.UncolorizedColumns()
ellipses := ellipsesArray[(data.tick+data.display.currentTick)%len(ellipsesArray)]
if uncolorizedColumns[infoColumn] == "" {
return data.step.Op.Color() + ellipses + colors.Reset
}
return ellipses
}
return ""
}
func (data *resourceRowData) ColorizedColumns() []string {
if len(data.columns) == 0 {
columns := data.getUnpaddedColumns()
data.columns = columns
}
return data.columns
}
// Gets the single line summary to show for a resource. This will include the current state of
// the resource (i.e. "Creating", "Replaced", "Failed", etc.) as well as relevant diagnostic
// information if there is any.
func (data *resourceRowData) getUnpaddedColumns() []string {
step := data.step
if step.Op == "" {
contract.Failf("Finishing a resource we never heard about: '%s'", data.id)
}
var name string
var typ string
if data.step.URN == "" {
name = "global"
typ = "global"
} else {
name = string(data.step.URN.Name())
typ = simplifyTypeName(data.step.URN.Type())
}
columns := make([]string, 5)
columns[idColumn] = data.id
columns[typeColumn] = typ
columns[nameColumn] = name
diagInfo := data.diagInfo
if data.done {
failed := data.failed || diagInfo.ErrorCount > 0
columns[statusColumn] = data.display.getStepDoneDescription(step, failed)
} else {
columns[statusColumn] = data.display.getStepInProgressDescription(step)
}
columns[infoColumn] = data.getInfo()
return columns
}
func (data *resourceRowData) getInfo() string {
step := data.step
changesBuf := &bytes.Buffer{}
if step.Old != nil && step.New != nil && step.Old.Inputs != nil && step.New.Inputs != nil {
diff := step.Old.Inputs.Diff(step.New.Inputs)
if diff != nil {
writeString(changesBuf, "changes:")
updates := make(resource.PropertyMap)
for k := range diff.Updates {
updates[k] = resource.PropertyValue{}
}
writePropertyKeys(changesBuf, diff.Adds, deploy.OpCreate)
writePropertyKeys(changesBuf, diff.Deletes, deploy.OpDelete)
writePropertyKeys(changesBuf, updates, deploy.OpReplace)
}
}
fprintIgnoreError(changesBuf, colors.Reset)
changes := changesBuf.String()
diagMsg := ""
if colors.Never.Colorize(changes) != "" {
diagMsg += changes
}
appendDiagMessage := func(msg string) {
if diagMsg != "" {
diagMsg += ", "
}
diagMsg += msg
}
diagInfo := data.diagInfo
if diagInfo.ErrorCount == 1 {
appendDiagMessage("1 error")
} else if diagInfo.ErrorCount > 1 {
appendDiagMessage(fmt.Sprintf("%v errors", diagInfo.ErrorCount))
}
if diagInfo.WarningCount == 1 {
appendDiagMessage("1 warning")
} else if diagInfo.WarningCount > 1 {
appendDiagMessage(fmt.Sprintf("%v warnings", diagInfo.WarningCount))
}
if diagInfo.InfoCount == 1 {
appendDiagMessage("1 info message")
} else if diagInfo.InfoCount > 1 {
appendDiagMessage(fmt.Sprintf("%v info messages", diagInfo.InfoCount))
}
if diagInfo.DebugCount == 1 {
appendDiagMessage("1 debug message")
} else if diagInfo.ErrorCount > 1 {
appendDiagMessage(fmt.Sprintf("%v debug messages", diagInfo.DebugCount))
}
// If we're not totally done, also print out the worst diagnostic next to the status message.
// This is helpful for long running tasks to know what's going on. However, once done, we print
// the diagnostics at the bottom, so we don't need to show this.
worstDiag := getWorstDiagnostic(data.diagInfo)
if worstDiag != nil && !data.display.Done {
eventMsg := data.display.renderProgressDiagEvent(*worstDiag)
if eventMsg != "" {
diagMsg += ". " + eventMsg
}
}
return diagMsg
}
// Returns the worst diagnostic we've seen. Used to produce a diagnostic string to go along with
// any resource if it has had any issues.
func getWorstDiagnostic(diagInfo *DiagInfo) *engine.Event {
if diagInfo.LastError != nil {
return diagInfo.LastError
}
if diagInfo.LastWarning != nil {
return diagInfo.LastWarning
}
if diagInfo.LastInfoError != nil {
return diagInfo.LastInfoError
}
if diagInfo.LastInfo != nil {
return diagInfo.LastInfo
}
return diagInfo.LastDebug
}
func uncolorise(columns []string) []string {
uncolorizedColumns := make([]string, len(columns))
for i, v := range columns {
uncolorizedColumns[i] = colors.Never.Colorize(v)
}
return uncolorizedColumns
}
func (data *resourceRowData) UncolorizedColumns() []string {
if len(data.uncolorizedColumns) == 0 {
columns := data.ColorizedColumns()
data.uncolorizedColumns = uncolorise(columns)
}
return data.uncolorizedColumns
}