pulumi/pkg/backend/display/progress.go

1452 lines
46 KiB
Go
Raw Normal View History

2018-05-22 21:43:36 +02:00
// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// nolint: goconst
package display
import (
"bytes"
"fmt"
"io"
"math"
"os"
"sort"
"strings"
"time"
"unicode"
"unicode/utf8"
"github.com/docker/docker/pkg/term"
"golang.org/x/crypto/ssh/terminal"
"github.com/pulumi/pulumi/pkg/v3/engine"
"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
// Progress describes a message we want to show in the display. There are two types of messages,
// simple 'Messages' which just get printed out as a single uninterpreted line, and 'Actions' which
// are placed and updated in the progress-grid based on their ID. Messages do not need an ID, while
// Actions must have an ID.
type Progress struct {
ID string
Message string
Action string
}
func makeMessageProgress(message string) Progress {
return Progress{Message: message}
}
func makeActionProgress(id string, action string) Progress {
contract.Assertf(id != "", "id must be non empty for action %s", action)
contract.Assertf(action != "", "action must be non empty")
return Progress{ID: id, Action: action}
}
// DiagInfo contains the bundle of diagnostic information for a single resource.
type DiagInfo struct {
ErrorCount, WarningCount, InfoCount, DebugCount int
// The very last diagnostic event we got for this resource (regardless of severity). We'll print
// this out in the non-interactive mode whenever we get new events. Importantly, we don't want
// to print out the most significant diagnostic, as that means a flurry of event swill cause us
// to keep printing out the most significant diagnostic over and over again.
LastDiag *engine.DiagEventPayload
// The last error we received. If we have an error, and we're in tree-view, we'll prefer to
// show this over the last non-error diag so that users know about something bad early on.
LastError *engine.DiagEventPayload
// 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 print out all
// diagnostics for a resource.
//
// Diagnostic events are bucketed by their associated stream ID (with 0 being the default
// stream).
StreamIDToDiagPayloads map[int32][]engine.DiagEventPayload
}
// ProgressDisplay organizes all the information needed for a dynamically updated "progress" view of an update.
2018-04-17 08:41:00 +02:00
type ProgressDisplay struct {
opts Options
progressOutput chan<- Progress
2018-04-17 08:41:00 +02:00
// action is the kind of action (preview, update, refresh, etc) being performed.
action apitype.UpdateKind
Make a smattering of CLI UX improvements Since I was digging around over the weekend after the change to move away from light black, and the impact it had on less important information showing more prominently than it used to, I took a step back and did a deeper tidying up of things. Another side goal of this exercise was to be a little more respectful of terminal width; when we could say things with fewer words, I did so. * Stylize the preview/update summary differently, so that it stands out as a section. Also highlight the total changes with bold -- it turns out this has a similar effect to the bright white colorization, just without the negative effects on e.g. white terminals. * Eliminate some verbosity in the phrasing of change summaries. * Make all heading sections stylized consistently. This includes the color (bright magenta) and the vertical spacing (always a newline separating headings). We were previously inconsistent on this (e.g., outputs were under "---outputs---"). Now the headings are: Previewing (etc), Diagnostics, Outputs, Resources, Duration, and Permalink. * Fix an issue where we'd parent things to "global" until the stack object later showed up. Now we'll simply mock up a stack resource. * Don't show messages like "no change" or "unchanged". Prior to the light black removal, these faded into the background of the terminal. Now they just clutter up the display. Similar to the elision of "*" for OpSames in a prior commit, just leave these out. Now anything that's written is actually a meaningful status for the user to note. * Don't show the "3 info messages," etc. summaries in the Info column while an update is ongoing. Instead, just show the latest line. This is more respectful of width -- I often find that the important messages scroll off the right of my screen before this change. For discussion: - I actually wonder if we should eliminate the summary altogether and always just show the latest line. Or even blank it out. The summary feels better suited for the Diagnostics section, and the Status concisely tells us how a resource's update ended up (failed, succeeded, etc). - Similarly, I question the idea of showing only the "worst" message. I'd vote for always showing the latest, and again leaving it to the Status column for concisely telling the user about the final state a resource ended up in. * Stop prepending "info: " to every stdout/stderr message. It adds no value, clutters up the display, and worsens horizontal usage. * Lessen the verbosity of update headline messages, so we now instead of e.g. "Previewing update of stack 'x':", we just say "Previewing update (x):". * Eliminate vertical whitespace in the Diagnostics section. Every independent console.out previously was separated by an entire newline, which made the section look cluttered to my eyes. These are just streams of logs, there's no reason for the extra newlines. * Colorize the resource headers in the Diagnostic section light blue. Note that this will change various test baselines, which I will update next. I didn't want those in the same commit.
2018-09-24 17:31:19 +02:00
// stack is the stack this progress pertains to.
stack tokens.QName
// proj is the project this progress pertains to.
proj tokens.PackageName
2018-04-17 08:41:00 +02:00
// Whether or not we're previewing. We don't know what we are actually doing until
// we get the initial 'prelude' event.
//
// this flag is only used to adjust how we describe what's going on to the user.
// i.e. if we're previewing we say things like "Would update" instead of "Updating".
isPreview bool
// The urn of the stack.
stackUrn resource.URN
// Whether or not we've seen outputs for the stack yet.
seenStackOutputs bool
2018-04-17 08:41:00 +02:00
// The summary event from the engine. If we get this, we'll print this after all
// normal resource events are heard. That way we don't interfere with all the progress
// messages we're outputting for them.
summaryEventPayload *engine.SummaryEventPayload
// Any system events we've received. They will be printed at the bottom of all the status rows
systemEventPayloads []engine.StdoutEventPayload
2018-04-17 08:41:00 +02:00
// Used to record the order that rows are created in. That way, when we present in a tree, we
// can keep things ordered so they will not jump around.
displayOrderCounter int
2018-04-17 08:41:00 +02:00
// 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
// A spinner to use to show that we're still doing work even when no output has been
// printed to the console in a while.
nonInteractiveSpinner cmdutil.Spinner
2018-04-23 03:10:19 +02:00
headerRow Row
resourceRows []ResourceRow
2018-04-17 08:41:00 +02:00
// A mapping from each resource URN we are told about to its current 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
// align. We don't need to do that in non-terminal situations.
isTerminal bool
// The width and height of the terminal. Used so we can trim resource messages that are too long.
terminalWidth int
terminalHeight int
2018-04-17 08:41:00 +02:00
// If all progress messages are done and we can print out the final display.
done bool
2018-04-17 08:41:00 +02:00
// The column that the suffix should be added to
suffixColumn int
// the list of suffixes to rotate through
suffixesArray []string
// Maps used so we can generate short IDs for resource urns.
urnToID map[resource.URN]string
// Cache of colorized to uncolorized text. We go between the two a lot, so caching helps
// prevent lots of recomputation
colorizedToUncolorized map[string]string
// Cache of lines we've already printed. We don't print a progress message again if it hasn't
// changed between the last time we printed and now.
printedProgressCache map[string]Progress
2018-04-17 08:41:00 +02:00
}
var (
// policyPayloads is a collection of policy violation events for a single resource.
policyPayloads []engine.PolicyViolationEventPayload
)
func camelCase(s string) string {
if len(s) == 0 {
return s
}
runes := []rune(s)
runes[0] = unicode.ToLower(runes[0])
return string(runes)
}
func simplifyTypeName(typ tokens.Type) string {
typeString := string(typ)
components := strings.Split(typeString, ":")
if len(components) != 3 {
return typeString
}
pkg, module, name := components[0], components[1], components[2]
if len(name) == 0 {
return typeString
}
lastSlashInModule := strings.LastIndexByte(module, '/')
if lastSlashInModule == -1 {
return typeString
}
file := module[lastSlashInModule+1:]
if file != camelCase(name) {
return typeString
}
return fmt.Sprintf("%v:%v:%v", pkg, module[:lastSlashInModule], name)
}
// getEventUrn returns the resource URN associated with an event, or the empty URN if this is not an
// event that has a URN. If this is also a 'step' event, then this will return the step metadata as
// well.
func getEventUrnAndMetadata(event engine.Event) (resource.URN, *engine.StepEventMetadata) {
switch event.Type {
case engine.ResourcePreEvent:
payload := event.Payload().(engine.ResourcePreEventPayload)
return payload.Metadata.URN, &payload.Metadata
case engine.ResourceOutputsEvent:
payload := event.Payload().(engine.ResourceOutputsEventPayload)
return payload.Metadata.URN, &payload.Metadata
case engine.ResourceOperationFailed:
payload := event.Payload().(engine.ResourceOperationFailedPayload)
return payload.Metadata.URN, &payload.Metadata
case engine.DiagEvent:
return event.Payload().(engine.DiagEventPayload).URN, nil
case engine.PolicyViolationEvent:
return event.Payload().(engine.PolicyViolationEventPayload).ResourceURN, nil
default:
return "", nil
}
}
// Converts the colorization tags in a progress message and then actually writes the progress
// message to the output stream. This should be the only place in this file where we actually
// process colorization tags.
func (display *ProgressDisplay) colorizeAndWriteProgress(progress Progress) {
if progress.Message != "" {
progress.Message = display.opts.Color.Colorize(progress.Message)
}
if progress.Action != "" {
progress.Action = display.opts.Color.Colorize(progress.Action)
}
if progress.ID != "" {
// don't repeat the same output if there is no difference between the last time we
// printed it and now.
lastProgress, has := display.printedProgressCache[progress.ID]
if has && lastProgress.Message == progress.Message && lastProgress.Action == progress.Action {
return
}
display.printedProgressCache[progress.ID] = progress
}
if !display.isTerminal {
// We're about to display something. Reset our spinner so that it will go on the next line.
display.nonInteractiveSpinner.Reset()
}
display.progressOutput <- progress
}
func (display *ProgressDisplay) writeSimpleMessage(msg string) {
display.colorizeAndWriteProgress(makeMessageProgress(msg))
}
func (display *ProgressDisplay) writeBlankLine() {
display.writeSimpleMessage(" ")
}
2018-09-05 17:25:23 +02:00
// ShowProgressEvents displays the engine events with docker's progress view.
Make a smattering of CLI UX improvements Since I was digging around over the weekend after the change to move away from light black, and the impact it had on less important information showing more prominently than it used to, I took a step back and did a deeper tidying up of things. Another side goal of this exercise was to be a little more respectful of terminal width; when we could say things with fewer words, I did so. * Stylize the preview/update summary differently, so that it stands out as a section. Also highlight the total changes with bold -- it turns out this has a similar effect to the bright white colorization, just without the negative effects on e.g. white terminals. * Eliminate some verbosity in the phrasing of change summaries. * Make all heading sections stylized consistently. This includes the color (bright magenta) and the vertical spacing (always a newline separating headings). We were previously inconsistent on this (e.g., outputs were under "---outputs---"). Now the headings are: Previewing (etc), Diagnostics, Outputs, Resources, Duration, and Permalink. * Fix an issue where we'd parent things to "global" until the stack object later showed up. Now we'll simply mock up a stack resource. * Don't show messages like "no change" or "unchanged". Prior to the light black removal, these faded into the background of the terminal. Now they just clutter up the display. Similar to the elision of "*" for OpSames in a prior commit, just leave these out. Now anything that's written is actually a meaningful status for the user to note. * Don't show the "3 info messages," etc. summaries in the Info column while an update is ongoing. Instead, just show the latest line. This is more respectful of width -- I often find that the important messages scroll off the right of my screen before this change. For discussion: - I actually wonder if we should eliminate the summary altogether and always just show the latest line. Or even blank it out. The summary feels better suited for the Diagnostics section, and the Status concisely tells us how a resource's update ended up (failed, succeeded, etc). - Similarly, I question the idea of showing only the "worst" message. I'd vote for always showing the latest, and again leaving it to the Status column for concisely telling the user about the final state a resource ended up in. * Stop prepending "info: " to every stdout/stderr message. It adds no value, clutters up the display, and worsens horizontal usage. * Lessen the verbosity of update headline messages, so we now instead of e.g. "Previewing update of stack 'x':", we just say "Previewing update (x):". * Eliminate vertical whitespace in the Diagnostics section. Every independent console.out previously was separated by an entire newline, which made the section look cluttered to my eyes. These are just streams of logs, there's no reason for the extra newlines. * Colorize the resource headers in the Diagnostic section light blue. Note that this will change various test baselines, which I will update next. I didn't want those in the same commit.
2018-09-24 17:31:19 +02:00
func ShowProgressEvents(op string, action apitype.UpdateKind, stack tokens.QName, proj tokens.PackageName,
events <-chan engine.Event, done chan<- bool, opts Options, isPreview bool) {
stdout := opts.Stdout
if stdout == nil {
stdout = os.Stdout
}
stderr := opts.Stderr
if stderr == nil {
stderr = os.Stderr
}
// Create a ticker that will update all our status messages once a second. Any
// in-flight resources will get a varying . .. ... ticker appended to them to
// let the user know what is still being worked on.
var spinner cmdutil.Spinner
var ticker *time.Ticker
if stdout == os.Stdout && stderr == os.Stderr && opts.IsInteractive {
spinner, ticker = cmdutil.NewSpinnerAndTicker(
fmt.Sprintf("%s%s...", cmdutil.EmojiOr("✨ ", "@ "), op),
nil, 1 /*timesPerSecond*/)
} else {
spinner = &nopSpinner{}
ticker = time.NewTicker(math.MaxInt64)
}
2018-09-05 17:25:23 +02:00
// The channel we push progress messages into, and which ShowProgressOutput pulls
// from to display to the console.
progressOutput := make(chan Progress)
display := &ProgressDisplay{
action: action,
isPreview: isPreview,
opts: opts,
Make a smattering of CLI UX improvements Since I was digging around over the weekend after the change to move away from light black, and the impact it had on less important information showing more prominently than it used to, I took a step back and did a deeper tidying up of things. Another side goal of this exercise was to be a little more respectful of terminal width; when we could say things with fewer words, I did so. * Stylize the preview/update summary differently, so that it stands out as a section. Also highlight the total changes with bold -- it turns out this has a similar effect to the bright white colorization, just without the negative effects on e.g. white terminals. * Eliminate some verbosity in the phrasing of change summaries. * Make all heading sections stylized consistently. This includes the color (bright magenta) and the vertical spacing (always a newline separating headings). We were previously inconsistent on this (e.g., outputs were under "---outputs---"). Now the headings are: Previewing (etc), Diagnostics, Outputs, Resources, Duration, and Permalink. * Fix an issue where we'd parent things to "global" until the stack object later showed up. Now we'll simply mock up a stack resource. * Don't show messages like "no change" or "unchanged". Prior to the light black removal, these faded into the background of the terminal. Now they just clutter up the display. Similar to the elision of "*" for OpSames in a prior commit, just leave these out. Now anything that's written is actually a meaningful status for the user to note. * Don't show the "3 info messages," etc. summaries in the Info column while an update is ongoing. Instead, just show the latest line. This is more respectful of width -- I often find that the important messages scroll off the right of my screen before this change. For discussion: - I actually wonder if we should eliminate the summary altogether and always just show the latest line. Or even blank it out. The summary feels better suited for the Diagnostics section, and the Status concisely tells us how a resource's update ended up (failed, succeeded, etc). - Similarly, I question the idea of showing only the "worst" message. I'd vote for always showing the latest, and again leaving it to the Status column for concisely telling the user about the final state a resource ended up in. * Stop prepending "info: " to every stdout/stderr message. It adds no value, clutters up the display, and worsens horizontal usage. * Lessen the verbosity of update headline messages, so we now instead of e.g. "Previewing update of stack 'x':", we just say "Previewing update (x):". * Eliminate vertical whitespace in the Diagnostics section. Every independent console.out previously was separated by an entire newline, which made the section look cluttered to my eyes. These are just streams of logs, there's no reason for the extra newlines. * Colorize the resource headers in the Diagnostic section light blue. Note that this will change various test baselines, which I will update next. I didn't want those in the same commit.
2018-09-24 17:31:19 +02:00
stack: stack,
proj: proj,
progressOutput: progressOutput,
eventUrnToResourceRow: make(map[resource.URN]ResourceRow),
suffixColumn: int(statusColumn),
suffixesArray: []string{"", ".", "..", "..."},
urnToID: make(map[resource.URN]string),
colorizedToUncolorized: make(map[string]string),
printedProgressCache: make(map[string]Progress),
displayOrderCounter: 1,
nonInteractiveSpinner: spinner,
2018-04-17 08:41:00 +02:00
}
// Assume we are not displaying in a terminal by default.
display.isTerminal = false
if stdout == os.Stdout {
terminalWidth, terminalHeight, err := terminal.GetSize(int(os.Stdout.Fd()))
if err == nil {
// If the terminal has a size, use it.
display.isTerminal = opts.IsInteractive
display.terminalWidth = terminalWidth
display.terminalHeight = terminalHeight
// Don't bother attempting to treat this display as a terminal if it has no width/height.
if display.isTerminal && (display.terminalWidth == 0 || display.terminalHeight == 0) {
display.isTerminal = false
_, err = fmt.Fprintln(stderr, "Treating display as non-terminal due to 0 width/height.")
contract.IgnoreError(err)
}
// Fetch the canonical stdout stream, configured appropriately.
_, stdout, _ = term.StdStreams()
}
}
go func() {
display.processEvents(ticker, events)
// no more progress events from this point on. By closing the pipe, this will then cause
// DisplayJSONMessagesToStream to finish once it processes the last message is receives from
// pipeReader, causing DisplayEvents to finally complete.
close(progressOutput)
}()
2018-09-05 17:25:23 +02:00
ShowProgressOutput(progressOutput, stdout, display.isTerminal)
ticker.Stop()
// let our caller know we're done.
close(done)
}
// Gets the padding necessary to prepend to a message in order to keep it aligned in the
// terminal.
2018-04-23 03:10:19 +02:00
func (display *ProgressDisplay) getMessagePadding(
uncolorizedColumns []string, columnIndex int, maxColumnLengths []int) string {
2018-04-23 03:10:19 +02:00
extraWhitespace := " "
if columnIndex >= 0 && display.isTerminal {
column := uncolorizedColumns[columnIndex]
maxLength := maxColumnLengths[columnIndex]
extraWhitespace = messagePadding(column, maxLength, 2)
}
return extraWhitespace
}
// Gets the fully padded message to be shown. The message will always include the ID of the
// 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(
colorizedColumns, uncolorizedColumns []string, maxColumnLengths []int) string {
colorizedMessage := ""
2018-04-23 03:10:19 +02:00
for i := 0; i < len(colorizedColumns); i++ {
padding := display.getMessagePadding(uncolorizedColumns, i-1, maxColumnLengths)
2018-04-17 08:41:00 +02:00
colorizedMessage += padding + colorizedColumns[i]
}
if display.isTerminal {
// 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 - 1
if maxMsgLength < 0 {
maxMsgLength = 0
}
colorizedMessage = colors.TrimColorizedString(colorizedMessage, maxMsgLength)
}
2018-04-17 08:41:00 +02:00
return colorizedMessage
}
func (display *ProgressDisplay) uncolorizeString(v string) string {
uncolorized, has := display.colorizedToUncolorized[v]
if !has {
uncolorized = colors.Never.Colorize(v)
display.colorizedToUncolorized[v] = uncolorized
}
return uncolorized
}
func (display *ProgressDisplay) uncolorizeColumns(columns []string) []string {
2018-04-17 08:41:00 +02:00
uncolorizedColumns := make([]string, len(columns))
for i, v := range columns {
uncolorizedColumns[i] = display.uncolorizeString(v)
2018-04-17 08:41:00 +02:00
}
return uncolorizedColumns
}
func (display *ProgressDisplay) refreshSingleRow(id string, row Row, maxColumnLengths []int) {
colorizedColumns := row.ColorizedColumns()
2018-04-17 08:41:00 +02:00
colorizedColumns[display.suffixColumn] += row.ColorizedSuffix()
display.refreshColumns(id, colorizedColumns, maxColumnLengths)
}
func (display *ProgressDisplay) refreshColumns(
id string, colorizedColumns []string, maxColumnLengths []int) {
2018-04-17 08:41:00 +02:00
uncolorizedColumns := display.uncolorizeColumns(colorizedColumns)
2018-04-17 08:41:00 +02:00
msg := display.getPaddedMessage(colorizedColumns, uncolorizedColumns, maxColumnLengths)
2018-04-23 03:10:19 +02:00
if display.isTerminal {
display.colorizeAndWriteProgress(makeActionProgress(id, msg))
2018-04-23 03:10:19 +02:00
} else {
display.writeSimpleMessage(msg)
}
}
// Ensure our stored dimension info is up to date.
func (display *ProgressDisplay) updateTerminalDimensions() {
// don't do any refreshing if we're not in a terminal
if display.isTerminal {
currentTerminalWidth, currentTerminalHeight, err := terminal.GetSize(int(os.Stdout.Fd()))
contract.IgnoreError(err)
if currentTerminalWidth != display.terminalWidth ||
currentTerminalHeight != display.terminalHeight {
display.terminalWidth = currentTerminalWidth
display.terminalHeight = currentTerminalHeight
// also clear our display cache as we want to reprint all lines.
display.printedProgressCache = make(map[string]Progress)
}
}
}
type treeNode struct {
row Row
colorizedColumns []string
colorizedSuffix string
childNodes []*treeNode
}
func (display *ProgressDisplay) getOrCreateTreeNode(
result *[]*treeNode, urn resource.URN, row ResourceRow, urnToTreeNode map[resource.URN]*treeNode) *treeNode {
node, has := urnToTreeNode[urn]
if has {
return node
}
node = &treeNode{
row: row,
colorizedColumns: row.ColorizedColumns(),
colorizedSuffix: row.ColorizedSuffix(),
}
urnToTreeNode[urn] = node
// if it's the not the root item, attach it as a child node to an appropriate parent item.
if urn != "" && urn != display.stackUrn {
var parentURN resource.URN
res := row.Step().Res
if res != nil {
parentURN = res.Parent
}
parentRow, hasParentRow := display.eventUrnToResourceRow[parentURN]
if !hasParentRow {
// If we haven't heard about this node's parent, then just parent it to the stack.
// Note: getting the parent row for the stack-urn will always succeed as we ensure that
// such a row is always there in ensureHeaderAndStackRows
parentURN = display.stackUrn
parentRow = display.eventUrnToResourceRow[parentURN]
}
parentNode := display.getOrCreateTreeNode(result, parentURN, parentRow, urnToTreeNode)
parentNode.childNodes = append(parentNode.childNodes, node)
return node
}
*result = append(*result, node)
return node
}
func (display *ProgressDisplay) generateTreeNodes() []*treeNode {
result := []*treeNode{}
result = append(result, &treeNode{
row: display.headerRow,
colorizedColumns: display.headerRow.ColorizedColumns(),
})
urnToTreeNode := make(map[resource.URN]*treeNode)
for urn, row := range display.eventUrnToResourceRow {
display.getOrCreateTreeNode(&result, urn, row, urnToTreeNode)
}
return result
}
func (display *ProgressDisplay) addIndentations(treeNodes []*treeNode, isRoot bool, indentation string) {
childIndentation := indentation + "│ "
lastChildIndentation := indentation + " "
for i, node := range treeNodes {
isLast := i == len(treeNodes)-1
prefix := indentation
var nestedIndentation string
if !isRoot {
if isLast {
prefix += "└─ "
nestedIndentation = lastChildIndentation
} else {
prefix += "├─ "
nestedIndentation = childIndentation
}
}
node.colorizedColumns[typeColumn] = prefix + node.colorizedColumns[typeColumn]
display.addIndentations(node.childNodes, false /*isRoot*/, nestedIndentation)
}
}
2018-04-17 08:41:00 +02:00
func (display *ProgressDisplay) convertNodesToRows(
nodes []*treeNode, maxSuffixLength int, rows *[][]string, maxColumnLengths *[]int) {
for _, node := range nodes {
if len(*maxColumnLengths) == 0 {
*maxColumnLengths = make([]int, len(node.colorizedColumns))
}
colorizedColumns := make([]string, len(node.colorizedColumns))
uncolorisedColumns := display.uncolorizeColumns(node.colorizedColumns)
for i, colorizedColumn := range node.colorizedColumns {
columnWidth := utf8.RuneCountInString(uncolorisedColumns[i])
if i == display.suffixColumn {
columnWidth += maxSuffixLength
colorizedColumns[i] = colorizedColumn + node.colorizedSuffix
} else {
colorizedColumns[i] = colorizedColumn
}
if columnWidth > (*maxColumnLengths)[i] {
(*maxColumnLengths)[i] = columnWidth
}
}
*rows = append(*rows, colorizedColumns)
display.convertNodesToRows(node.childNodes, maxSuffixLength, rows, maxColumnLengths)
}
2018-04-23 03:10:19 +02:00
}
type sortable []*treeNode
func (sortable sortable) Len() int {
return len(sortable)
}
func (sortable sortable) Less(i, j int) bool {
return sortable[i].row.DisplayOrderIndex() < sortable[j].row.DisplayOrderIndex()
}
func (sortable sortable) Swap(i, j int) {
sortable[i], sortable[j] = sortable[j], sortable[i]
}
func sortNodes(nodes []*treeNode) {
sort.Sort(sortable(nodes))
for _, node := range nodes {
childNodes := node.childNodes
sortNodes(childNodes)
node.childNodes = childNodes
2018-04-23 03:10:19 +02:00
}
}
func (display *ProgressDisplay) filterOutUnnecessaryNodesAndSetDisplayTimes(nodes []*treeNode) []*treeNode {
result := []*treeNode{}
for _, node := range nodes {
node.childNodes = display.filterOutUnnecessaryNodesAndSetDisplayTimes(node.childNodes)
if node.row.HideRowIfUnnecessary() && len(node.childNodes) == 0 {
continue
}
display.displayOrderCounter++
node.row.SetDisplayOrderIndex(display.displayOrderCounter)
result = append(result, node)
2018-04-23 03:10:19 +02:00
}
return result
}
func (display *ProgressDisplay) refreshAllRowsIfInTerminal() {
2018-04-23 03:10:19 +02:00
if display.isTerminal && display.headerRow != nil {
// make sure our stored dimension info is up to date
display.updateTerminalDimensions()
rootNodes := display.generateTreeNodes()
rootNodes = display.filterOutUnnecessaryNodesAndSetDisplayTimes(rootNodes)
sortNodes(rootNodes)
display.addIndentations(rootNodes, true /*isRoot*/, "")
maxSuffixLength := 0
for _, v := range display.suffixesArray {
runeCount := utf8.RuneCountInString(v)
if runeCount > maxSuffixLength {
maxSuffixLength = runeCount
}
}
var rows [][]string
var maxColumnLengths []int
display.convertNodesToRows(rootNodes, maxSuffixLength, &rows, &maxColumnLengths)
2018-04-23 03:10:19 +02:00
removeInfoColumnIfUnneeded(rows)
2018-04-23 03:10:19 +02:00
for i, row := range rows {
display.refreshColumns(fmt.Sprintf("%v", i), row, maxColumnLengths)
}
2018-04-23 03:10:19 +02:00
systemID := len(rows)
printedHeader := false
for _, payload := range display.systemEventPayloads {
msg := payload.Color.Colorize(payload.Message)
lines := splitIntoDisplayableLines(msg)
if len(lines) == 0 {
continue
}
if !printedHeader {
printedHeader = true
display.colorizeAndWriteProgress(makeActionProgress(
fmt.Sprintf("%v", systemID), " "))
systemID++
display.colorizeAndWriteProgress(makeActionProgress(
fmt.Sprintf("%v", systemID),
colors.Yellow+"System Messages"+colors.Reset))
systemID++
}
for _, line := range lines {
display.colorizeAndWriteProgress(makeActionProgress(
fmt.Sprintf("%v", systemID), fmt.Sprintf(" %s", line)))
systemID++
}
}
}
}
func removeInfoColumnIfUnneeded(rows [][]string) {
// If there have been no info messages, then don't print out the info column header.
for i := 1; i < len(rows); i++ {
row := rows[i]
if row[len(row)-1] != "" {
return
}
}
firstRow := rows[0]
firstRow[len(firstRow)-1] = ""
}
// Performs all the work at the end once we've heard about the last message from the engine.
// Specifically, this will update the status messages for any resources, and will also then
// print out all final diagnostics. and finally will print out the summary.
func (display *ProgressDisplay) processEndSteps() {
// Figure out the rows that are currently in progress.
inProgressRows := []ResourceRow{}
for _, v := range display.eventUrnToResourceRow {
if !v.IsDone() {
inProgressRows = append(inProgressRows, v)
}
}
// Transition the display to the 'done' state. This will transitively cause all
// rows to become done.
display.done = true
// Now print out all those rows that were in progress. They will now be 'done'
// since the display was marked 'done'.
if !display.isTerminal {
for _, v := range inProgressRows {
display.refreshSingleRow("", v, nil)
}
}
// 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 is 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()
// Render several "sections" of output based on available data as applicable.
display.writeBlankLine()
wroteDiagnosticHeader := display.printDiagnostics()
wrotePolicyViolations := display.printPolicyViolations()
display.printOutputs()
// If no policies violated, print policy packs applied.
if !wrotePolicyViolations {
display.printSummary(wroteDiagnosticHeader)
}
}
// printDiagnostics prints a new "Diagnostics:" section with all of the diagnostics grouped by
// resource. If no diagnostics were emitted, prints nothing.
func (display *ProgressDisplay) printDiagnostics() bool {
// Since we display diagnostic information eagerly, we need to keep track of the first
// time we wrote some output so we don't inadvertently print the header twice.
wroteDiagnosticHeader := false
for _, row := range display.eventUrnToResourceRow {
// The header for the diagnogistics grouped by resource, e.g. "aws:apigateway:RestApi (accountsApi):"
wroteResourceHeader := false
// Each row in the display corresponded with a resource, and that resource could have emitted
// diagnostics to various streams.
for id, payloads := range row.DiagInfo().StreamIDToDiagPayloads {
if len(payloads) == 0 {
continue
}
if id != 0 {
// For the non-default stream merge all the messages from the stream into a single
// message.
p := display.mergeStreamPayloadsToSinglePayload(payloads)
payloads = []engine.DiagEventPayload{p}
}
// Did we write any diagnostic information for the resource x stream?
wrote := false
for _, v := range payloads {
if v.Ephemeral {
continue
}
msg := display.renderProgressDiagEvent(v, true /*includePrefix:*/)
lines := splitIntoDisplayableLines(msg)
if len(lines) == 0 {
continue
Make a smattering of CLI UX improvements Since I was digging around over the weekend after the change to move away from light black, and the impact it had on less important information showing more prominently than it used to, I took a step back and did a deeper tidying up of things. Another side goal of this exercise was to be a little more respectful of terminal width; when we could say things with fewer words, I did so. * Stylize the preview/update summary differently, so that it stands out as a section. Also highlight the total changes with bold -- it turns out this has a similar effect to the bright white colorization, just without the negative effects on e.g. white terminals. * Eliminate some verbosity in the phrasing of change summaries. * Make all heading sections stylized consistently. This includes the color (bright magenta) and the vertical spacing (always a newline separating headings). We were previously inconsistent on this (e.g., outputs were under "---outputs---"). Now the headings are: Previewing (etc), Diagnostics, Outputs, Resources, Duration, and Permalink. * Fix an issue where we'd parent things to "global" until the stack object later showed up. Now we'll simply mock up a stack resource. * Don't show messages like "no change" or "unchanged". Prior to the light black removal, these faded into the background of the terminal. Now they just clutter up the display. Similar to the elision of "*" for OpSames in a prior commit, just leave these out. Now anything that's written is actually a meaningful status for the user to note. * Don't show the "3 info messages," etc. summaries in the Info column while an update is ongoing. Instead, just show the latest line. This is more respectful of width -- I often find that the important messages scroll off the right of my screen before this change. For discussion: - I actually wonder if we should eliminate the summary altogether and always just show the latest line. Or even blank it out. The summary feels better suited for the Diagnostics section, and the Status concisely tells us how a resource's update ended up (failed, succeeded, etc). - Similarly, I question the idea of showing only the "worst" message. I'd vote for always showing the latest, and again leaving it to the Status column for concisely telling the user about the final state a resource ended up in. * Stop prepending "info: " to every stdout/stderr message. It adds no value, clutters up the display, and worsens horizontal usage. * Lessen the verbosity of update headline messages, so we now instead of e.g. "Previewing update of stack 'x':", we just say "Previewing update (x):". * Eliminate vertical whitespace in the Diagnostics section. Every independent console.out previously was separated by an entire newline, which made the section look cluttered to my eyes. These are just streams of logs, there's no reason for the extra newlines. * Colorize the resource headers in the Diagnostic section light blue. Note that this will change various test baselines, which I will update next. I didn't want those in the same commit.
2018-09-24 17:31:19 +02:00
}
// If we haven't printed the Diagnostics header, do so now.
if !wroteDiagnosticHeader {
wroteDiagnosticHeader = true
display.writeSimpleMessage(
display.opts.Color.Colorize(colors.SpecHeadline + "Diagnostics:" + colors.Reset))
}
// If we haven't printed the header for the resource, do so now.
if !wroteResourceHeader {
wroteResourceHeader = true
columns := row.ColorizedColumns()
display.writeSimpleMessage(" " +
display.opts.Color.Colorize(
colors.BrightBlue+columns[typeColumn]+" ("+columns[nameColumn]+"):"+colors.Reset))
}
for _, line := range lines {
line = strings.TrimRightFunc(line, unicode.IsSpace)
display.writeSimpleMessage(" " + line)
}
wrote = true
}
if wrote {
display.writeBlankLine()
}
}
}
return wroteDiagnosticHeader
}
// printPolicyViolations prints a new "Policy Violation:" section with all of the violations
// grouped by policy pack. If no policy violations were encountered, prints nothing.
func (display *ProgressDisplay) printPolicyViolations() bool {
// Loop through every resource and gather up all policy violations encountered.
var policyEvents []engine.PolicyViolationEventPayload
for _, row := range display.eventUrnToResourceRow {
policyPayloads := row.PolicyPayloads()
if len(policyPayloads) == 0 {
continue
}
policyEvents = append(policyEvents, policyPayloads...)
}
if len(policyEvents) == 0 {
return false
}
// Sort policy events by: policy pack name, policy pack version, enforcement level,
// policy name, and finally the URN of the resource.
sort.SliceStable(policyEvents, func(i, j int) bool {
eventI, eventJ := policyEvents[i], policyEvents[j]
if packNameCmp := strings.Compare(
eventI.PolicyPackName,
eventJ.PolicyPackName); packNameCmp != 0 {
return packNameCmp < 0
}
if packVerCmp := strings.Compare(
eventI.PolicyPackVersion,
eventJ.PolicyPackVersion); packVerCmp != 0 {
return packVerCmp < 0
}
if enfLevelCmp := strings.Compare(
string(eventI.EnforcementLevel),
string(eventJ.EnforcementLevel)); enfLevelCmp != 0 {
return enfLevelCmp < 0
}
if policyNameCmp := strings.Compare(
eventI.PolicyName,
eventJ.PolicyName); policyNameCmp != 0 {
return policyNameCmp < 0
}
urnCmp := strings.Compare(
string(eventI.ResourceURN),
string(eventJ.ResourceURN))
return urnCmp < 0
})
// Print every policy violation, printing a new header when necessary.
display.writeSimpleMessage(display.opts.Color.Colorize(colors.SpecHeadline + "Policy Violations:" + colors.Reset))
for _, policyEvent := range policyEvents {
// Print the individual policy event.
c := colors.SpecImportant
if policyEvent.EnforcementLevel == apitype.Mandatory {
c = colors.SpecError
2018-04-17 08:41:00 +02:00
}
policyNameLine := fmt.Sprintf(" %s[%s] %s v%s %s %s (%s: %s)",
c, policyEvent.EnforcementLevel,
policyEvent.PolicyPackName,
policyEvent.PolicyPackVersion, colors.Reset,
policyEvent.PolicyName,
policyEvent.ResourceURN.Type(),
policyEvent.ResourceURN.Name())
display.writeSimpleMessage(policyNameLine)
// The message may span multiple lines, so we massage it so it will be indented properly.
message := strings.ReplaceAll(policyEvent.Message, "\n", "\n ")
messageLine := fmt.Sprintf(" %s", message)
display.writeSimpleMessage(messageLine)
}
return true
}
// printOutputs prints the Stack's outputs for the display in a new section, if appropriate.
func (display *ProgressDisplay) printOutputs() {
// Printing the stack's outputs wasn't desired.
if display.opts.SuppressOutputs {
return
}
// Cannot display outputs for the stack if we don't know its URN.
if display.stackUrn == "" {
return
}
stackStep := display.eventUrnToResourceRow[display.stackUrn].Step()
props := engine.GetResourceOutputsPropertiesString(
stackStep, 1, display.isPreview, display.opts.Debug,
false /* refresh */, display.opts.ShowSameResources)
if props != "" {
display.writeSimpleMessage(colors.SpecHeadline + "Outputs:" + colors.Reset)
display.writeSimpleMessage(props)
}
}
// printSummary prints the Stack's SummaryEvent in a new section if applicable.
func (display *ProgressDisplay) printSummary(wroteDiagnosticHeader bool) {
// If we never saw the SummaryEvent payload, we have nothing to do.
if display.summaryEventPayload == nil {
return
}
msg := renderSummaryEvent(display.action, *display.summaryEventPayload, wroteDiagnosticHeader, display.opts)
display.writeSimpleMessage(msg)
}
func (display *ProgressDisplay) mergeStreamPayloadsToSinglePayload(
payloads []engine.DiagEventPayload) engine.DiagEventPayload {
buf := bytes.Buffer{}
for _, p := range payloads {
buf.WriteString(display.renderProgressDiagEvent(p, false /*includePrefix:*/))
}
firstPayload := payloads[0]
msg := buf.String()
return engine.DiagEventPayload{
URN: firstPayload.URN,
Message: msg,
Prefix: firstPayload.Prefix,
Color: firstPayload.Color,
Severity: firstPayload.Severity,
StreamID: firstPayload.StreamID,
Ephemeral: firstPayload.Ephemeral,
}
}
func splitIntoDisplayableLines(msg string) []string {
lines := strings.Split(msg, "\n")
// Trim off any trailing blank lines in the message.
for len(lines) > 0 {
lastLine := lines[len(lines)-1]
if strings.TrimSpace(colors.Never.Colorize(lastLine)) == "" {
lines = lines[0 : len(lines)-1]
} else {
break
}
}
return lines
}
func (display *ProgressDisplay) processTick() {
// Got a tick. Update the progress display if we're in a terminal. If we're not,
// print a hearbeat message every 10 seconds after our last output so that the user
// knows something is going on. This is also helpful for hosts like jenkins that
// often timeout a process if output is not seen in a while.
display.currentTick++
if display.isTerminal {
display.refreshAllRowsIfInTerminal()
} else {
// Update the spinner to let the user know that that work is still happening.
display.nonInteractiveSpinner.Tick()
}
}
func (display *ProgressDisplay) getRowForURN(urn resource.URN, metadata *engine.StepEventMetadata) ResourceRow {
// If there's already a row for this URN, return it.
row, has := display.eventUrnToResourceRow[urn]
if has {
return row
}
// First time we're hearing about this resource. Create an initial nearly-empty status for it.
step := engine.StepEventMetadata{URN: urn, Op: deploy.OpSame}
if metadata != nil {
step = *metadata
}
// If this is the first time we're seeing an event for the stack resource, check to see if we've already
// recorded root events that we want to reassociate with this URN.
if isRootURN(urn) {
display.stackUrn = urn
if row, has = display.eventUrnToResourceRow[""]; has {
row.SetStep(step)
display.eventUrnToResourceRow[urn] = row
delete(display.eventUrnToResourceRow, "")
return row
}
}
row = &resourceRowData{
display: display,
tick: display.currentTick,
diagInfo: &DiagInfo{},
policyPayloads: policyPayloads,
step: step,
hideRowIfUnnecessary: true,
}
display.eventUrnToResourceRow[urn] = row
display.ensureHeaderAndStackRows()
display.resourceRows = append(display.resourceRows, row)
return row
}
func (display *ProgressDisplay) processNormalEvent(event engine.Event) {
switch event.Type {
case engine.PreludeEvent:
// A prelude event can just be printed out directly to the console.
// Note: we should probably make sure we don't get any prelude events
// once we start hearing about actual resource events.
payload := event.Payload().(engine.PreludeEventPayload)
preludeEventString := renderPreludeEvent(payload, display.opts)
if display.isTerminal {
display.processNormalEvent(engine.NewEvent(engine.DiagEvent, engine.DiagEventPayload{
Ephemeral: false,
Severity: diag.Info,
Color: cmdutil.GetGlobalColorization(),
Message: preludeEventString,
}))
} else {
display.writeSimpleMessage(preludeEventString)
}
return
case engine.SummaryEvent:
// keep track of the summary event so that we can display it after all other
// resource-related events we receive.
payload := event.Payload().(engine.SummaryEventPayload)
display.summaryEventPayload = &payload
return
case engine.DiagEvent:
msg := display.renderProgressDiagEvent(event.Payload().(engine.DiagEventPayload), true /*includePrefix:*/)
if msg == "" {
return
}
case engine.StdoutColorEvent:
display.handleSystemEvent(event.Payload().(engine.StdoutEventPayload))
return
}
// At this point, all events should relate to resources.
eventUrn, metadata := getEventUrnAndMetadata(event)
// If we're suppressing reads from the tree-view, then convert notifications about reads into
// ephemeral messages that will go into the info column.
if metadata != nil && !display.opts.ShowReads {
if metadata.Op == deploy.OpReadDiscard || metadata.Op == deploy.OpReadReplacement {
// just flat out ignore read discards/replace. They're only relevant in the context of
// 'reads', and we only present reads as an ephemeral diagnostic anyways.
return
}
if metadata.Op == deploy.OpRead {
// Don't show reads as operations on a specific resource. It's an underlying detail
// that we don't want to clutter up the display with. However, to help users know
// what's going on, we can show them as ephemeral diagnostic messages that are
// associated at the top level with the stack. That way if things are taking a while,
// there's insight in the display as to what's going on.
display.processNormalEvent(engine.NewEvent(engine.DiagEvent, engine.DiagEventPayload{
Ephemeral: true,
Severity: diag.Info,
Color: cmdutil.GetGlobalColorization(),
Message: fmt.Sprintf("read %v %v", simplifyTypeName(eventUrn.Type()), eventUrn.Name()),
}))
return
}
}
if eventUrn == "" {
// If this event has no URN, associate it with the stack. Note that there may not yet be a stack resource, in
// which case this is a no-op.
eventUrn = display.stackUrn
}
isRootEvent := eventUrn == display.stackUrn
row := display.getRowForURN(eventUrn, metadata)
// Don't bother showing certain events (for example, things that are unchanged). However
// always show the root 'stack' resource so we can indicate that it's still running, and
// also so we have something to attach unparented diagnostic events to.
hideRowIfUnnecessary := metadata != nil && !shouldShow(*metadata, display.opts) && !isRootEvent
// Always show row if there's a policy violation event. Policy violations prevent resource
// registration, so if we don't show the row, the violation gets attributed to the stack
// resource rather than the resources whose policy failed.
hideRowIfUnnecessary = hideRowIfUnnecessary || event.Type == engine.PolicyViolationEvent
if !hideRowIfUnnecessary {
row.SetHideRowIfUnnecessary(false)
}
if event.Type == engine.ResourcePreEvent {
step := event.Payload().(engine.ResourcePreEventPayload).Metadata
row.SetStep(step)
} else if event.Type == engine.ResourceOutputsEvent {
isRefresh := display.getStepOp(row.Step()) == deploy.OpRefresh
step := event.Payload().(engine.ResourceOutputsEventPayload).Metadata
// Is this the stack outputs event? If so, we'll need to print it out at the end of the plan.
if step.URN == display.stackUrn {
display.seenStackOutputs = true
}
row.SetStep(step)
row.AddOutputStep(step)
// If we're not in a terminal, we may not want to display this row again: if we're displaying a preview or if
// this step is a no-op for a custom resource, refreshing this row will simply duplicate its earlier output.
hasMeaningfulOutput := isRefresh ||
!display.isPreview && (step.Res == nil || step.Res.Custom && step.Op != deploy.OpSame)
if !display.isTerminal && !hasMeaningfulOutput {
return
}
} else if event.Type == engine.ResourceOperationFailed {
row.SetFailed()
} else if event.Type == engine.DiagEvent {
// also record this diagnostic so we print it at the end.
row.RecordDiagEvent(event)
2019-06-11 00:20:44 +02:00
} else if event.Type == engine.PolicyViolationEvent {
// also record this policy violation so we print it at the end.
row.RecordPolicyViolationEvent(event)
} else {
contract.Failf("Unhandled event type '%s'", event.Type)
}
if display.isTerminal {
// if we're in a terminal, then refresh everything so that all our columns line up
display.refreshAllRowsIfInTerminal()
} else {
// otherwise, just print out this single row.
display.refreshSingleRow("", row, nil)
}
}
func (display *ProgressDisplay) handleSystemEvent(payload engine.StdoutEventPayload) {
// Make sure we have a header to display
display.ensureHeaderAndStackRows()
display.systemEventPayloads = append(display.systemEventPayloads, payload)
if display.isTerminal {
// if we're in a terminal, then refresh everything. The system events will come after
// all the normal rows
display.refreshAllRowsIfInTerminal()
} else {
// otherwise, in a non-terminal, just print out the actual event.
display.writeSimpleMessage(renderStdoutColorEvent(payload, display.opts))
}
}
func (display *ProgressDisplay) ensureHeaderAndStackRows() {
2018-04-23 03:10:19 +02:00
if display.headerRow == nil {
// about to make our first status message. make sure we present the header line first.
2018-04-23 03:10:19 +02:00
display.headerRow = &headerRowData{display: display}
}
// we've added at least one row to the table. make sure we have a row to designate the
// stack if we haven't already heard about it yet. This also ensures that as we build
// the tree we can always guarantee there's a 'root' to parent anything to.
_, hasStackRow := display.eventUrnToResourceRow[display.stackUrn]
if hasStackRow {
return
}
stackRow := &resourceRowData{
display: display,
tick: display.currentTick,
diagInfo: &DiagInfo{},
policyPayloads: policyPayloads,
step: engine.StepEventMetadata{Op: deploy.OpSame},
hideRowIfUnnecessary: false,
}
display.eventUrnToResourceRow[display.stackUrn] = stackRow
display.resourceRows = append(display.resourceRows, stackRow)
}
func (display *ProgressDisplay) processEvents(ticker *time.Ticker, events <-chan engine.Event) {
// 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:
display.processTick()
case event := <-events:
if event.Type == "" || event.Type == engine.CancelEvent {
// Engine finished sending events. Do all the final processing and return
// from this local func. This will print out things like full diagnostic
// events, as well as the summary event from the engine.
display.processEndSteps()
return
}
display.processNormalEvent(event)
}
}
}
func (display *ProgressDisplay) renderProgressDiagEvent(payload engine.DiagEventPayload, includePrefix bool) string {
if payload.Severity == diag.Debug && !display.opts.Debug {
return ""
}
msg := payload.Message
if includePrefix {
msg = payload.Prefix + msg
}
return strings.TrimRightFunc(msg, unicode.IsSpace)
}
func (display *ProgressDisplay) getStepDoneDescription(step engine.StepEventMetadata, failed bool) string {
makeError := func(v string) string {
return colors.SpecError + "**" + v + "**" + colors.Reset
}
op := display.getStepOp(step)
if display.isPreview {
// During a preview, when we transition to done, we'll print out summary text describing the step instead of a
// past-tense verb describing the step that was performed.
return op.Color() + display.getPreviewDoneText(step) + colors.Reset
}
getDescription := func() string {
if failed {
switch op {
case deploy.OpSame:
return "failed"
case deploy.OpCreate, deploy.OpCreateReplacement:
return "creating failed"
case deploy.OpUpdate:
return "updating failed"
case deploy.OpDelete, deploy.OpDeleteReplaced:
return "deleting failed"
case deploy.OpReplace:
return "replacing failed"
case deploy.OpRead, deploy.OpReadReplacement:
return "reading failed"
case deploy.OpRefresh:
return "refreshing failed"
case deploy.OpReadDiscard, deploy.OpDiscardReplaced:
return "discarding failed"
case deploy.OpImport, deploy.OpImportReplacement:
return "importing failed"
}
} else {
switch op {
case deploy.OpSame:
Make a smattering of CLI UX improvements Since I was digging around over the weekend after the change to move away from light black, and the impact it had on less important information showing more prominently than it used to, I took a step back and did a deeper tidying up of things. Another side goal of this exercise was to be a little more respectful of terminal width; when we could say things with fewer words, I did so. * Stylize the preview/update summary differently, so that it stands out as a section. Also highlight the total changes with bold -- it turns out this has a similar effect to the bright white colorization, just without the negative effects on e.g. white terminals. * Eliminate some verbosity in the phrasing of change summaries. * Make all heading sections stylized consistently. This includes the color (bright magenta) and the vertical spacing (always a newline separating headings). We were previously inconsistent on this (e.g., outputs were under "---outputs---"). Now the headings are: Previewing (etc), Diagnostics, Outputs, Resources, Duration, and Permalink. * Fix an issue where we'd parent things to "global" until the stack object later showed up. Now we'll simply mock up a stack resource. * Don't show messages like "no change" or "unchanged". Prior to the light black removal, these faded into the background of the terminal. Now they just clutter up the display. Similar to the elision of "*" for OpSames in a prior commit, just leave these out. Now anything that's written is actually a meaningful status for the user to note. * Don't show the "3 info messages," etc. summaries in the Info column while an update is ongoing. Instead, just show the latest line. This is more respectful of width -- I often find that the important messages scroll off the right of my screen before this change. For discussion: - I actually wonder if we should eliminate the summary altogether and always just show the latest line. Or even blank it out. The summary feels better suited for the Diagnostics section, and the Status concisely tells us how a resource's update ended up (failed, succeeded, etc). - Similarly, I question the idea of showing only the "worst" message. I'd vote for always showing the latest, and again leaving it to the Status column for concisely telling the user about the final state a resource ended up in. * Stop prepending "info: " to every stdout/stderr message. It adds no value, clutters up the display, and worsens horizontal usage. * Lessen the verbosity of update headline messages, so we now instead of e.g. "Previewing update of stack 'x':", we just say "Previewing update (x):". * Eliminate vertical whitespace in the Diagnostics section. Every independent console.out previously was separated by an entire newline, which made the section look cluttered to my eyes. These are just streams of logs, there's no reason for the extra newlines. * Colorize the resource headers in the Diagnostic section light blue. Note that this will change various test baselines, which I will update next. I didn't want those in the same commit.
2018-09-24 17:31:19 +02:00
return ""
case deploy.OpCreate:
return "created"
case deploy.OpUpdate:
return "updated"
case deploy.OpDelete:
return "deleted"
case deploy.OpReplace:
return "replaced"
case deploy.OpCreateReplacement:
return "created replacement"
case deploy.OpDeleteReplaced:
return "deleted original"
case deploy.OpRead:
// nolint: goconst
return "read"
case deploy.OpReadReplacement:
return "read for replacement"
case deploy.OpRefresh:
return "refresh"
case deploy.OpReadDiscard:
return "discarded"
case deploy.OpDiscardReplaced:
return "discarded original"
case deploy.OpImport:
return "imported"
case deploy.OpImportReplacement:
return "imported replacement"
}
}
contract.Failf("Unrecognized resource step op: %v", op)
return ""
}
if failed {
return makeError(getDescription())
}
return op.Color() + getDescription() + colors.Reset
}
func (display *ProgressDisplay) getPreviewText(step engine.StepEventMetadata) string {
switch step.Op {
case deploy.OpSame:
Make a smattering of CLI UX improvements Since I was digging around over the weekend after the change to move away from light black, and the impact it had on less important information showing more prominently than it used to, I took a step back and did a deeper tidying up of things. Another side goal of this exercise was to be a little more respectful of terminal width; when we could say things with fewer words, I did so. * Stylize the preview/update summary differently, so that it stands out as a section. Also highlight the total changes with bold -- it turns out this has a similar effect to the bright white colorization, just without the negative effects on e.g. white terminals. * Eliminate some verbosity in the phrasing of change summaries. * Make all heading sections stylized consistently. This includes the color (bright magenta) and the vertical spacing (always a newline separating headings). We were previously inconsistent on this (e.g., outputs were under "---outputs---"). Now the headings are: Previewing (etc), Diagnostics, Outputs, Resources, Duration, and Permalink. * Fix an issue where we'd parent things to "global" until the stack object later showed up. Now we'll simply mock up a stack resource. * Don't show messages like "no change" or "unchanged". Prior to the light black removal, these faded into the background of the terminal. Now they just clutter up the display. Similar to the elision of "*" for OpSames in a prior commit, just leave these out. Now anything that's written is actually a meaningful status for the user to note. * Don't show the "3 info messages," etc. summaries in the Info column while an update is ongoing. Instead, just show the latest line. This is more respectful of width -- I often find that the important messages scroll off the right of my screen before this change. For discussion: - I actually wonder if we should eliminate the summary altogether and always just show the latest line. Or even blank it out. The summary feels better suited for the Diagnostics section, and the Status concisely tells us how a resource's update ended up (failed, succeeded, etc). - Similarly, I question the idea of showing only the "worst" message. I'd vote for always showing the latest, and again leaving it to the Status column for concisely telling the user about the final state a resource ended up in. * Stop prepending "info: " to every stdout/stderr message. It adds no value, clutters up the display, and worsens horizontal usage. * Lessen the verbosity of update headline messages, so we now instead of e.g. "Previewing update of stack 'x':", we just say "Previewing update (x):". * Eliminate vertical whitespace in the Diagnostics section. Every independent console.out previously was separated by an entire newline, which made the section look cluttered to my eyes. These are just streams of logs, there's no reason for the extra newlines. * Colorize the resource headers in the Diagnostic section light blue. Note that this will change various test baselines, which I will update next. I didn't want those in the same commit.
2018-09-24 17:31:19 +02:00
return ""
case deploy.OpCreate:
2018-04-17 04:46:57 +02:00
return "create"
case deploy.OpUpdate:
2018-04-17 04:46:57 +02:00
return "update"
case deploy.OpDelete:
2018-04-17 04:46:57 +02:00
return "delete"
case deploy.OpReplace:
2018-04-17 04:46:57 +02:00
return "replace"
case deploy.OpCreateReplacement:
return "create replacement"
case deploy.OpDeleteReplaced:
return "delete original"
case deploy.OpRead:
// nolint: goconst
return "read"
case deploy.OpReadReplacement:
return "read for replacement"
case deploy.OpRefresh:
return "refreshing"
case deploy.OpReadDiscard:
return "discard"
case deploy.OpDiscardReplaced:
return "discard original"
case deploy.OpImport:
return "import"
case deploy.OpImportReplacement:
return "import replacement"
}
contract.Failf("Unrecognized resource step op: %v", step.Op)
return ""
}
// getPreviewDoneText returns a textual representation for this step, suitable for display during a preview once the
// preview has completed.
func (display *ProgressDisplay) getPreviewDoneText(step engine.StepEventMetadata) string {
switch step.Op {
case deploy.OpSame:
Make a smattering of CLI UX improvements Since I was digging around over the weekend after the change to move away from light black, and the impact it had on less important information showing more prominently than it used to, I took a step back and did a deeper tidying up of things. Another side goal of this exercise was to be a little more respectful of terminal width; when we could say things with fewer words, I did so. * Stylize the preview/update summary differently, so that it stands out as a section. Also highlight the total changes with bold -- it turns out this has a similar effect to the bright white colorization, just without the negative effects on e.g. white terminals. * Eliminate some verbosity in the phrasing of change summaries. * Make all heading sections stylized consistently. This includes the color (bright magenta) and the vertical spacing (always a newline separating headings). We were previously inconsistent on this (e.g., outputs were under "---outputs---"). Now the headings are: Previewing (etc), Diagnostics, Outputs, Resources, Duration, and Permalink. * Fix an issue where we'd parent things to "global" until the stack object later showed up. Now we'll simply mock up a stack resource. * Don't show messages like "no change" or "unchanged". Prior to the light black removal, these faded into the background of the terminal. Now they just clutter up the display. Similar to the elision of "*" for OpSames in a prior commit, just leave these out. Now anything that's written is actually a meaningful status for the user to note. * Don't show the "3 info messages," etc. summaries in the Info column while an update is ongoing. Instead, just show the latest line. This is more respectful of width -- I often find that the important messages scroll off the right of my screen before this change. For discussion: - I actually wonder if we should eliminate the summary altogether and always just show the latest line. Or even blank it out. The summary feels better suited for the Diagnostics section, and the Status concisely tells us how a resource's update ended up (failed, succeeded, etc). - Similarly, I question the idea of showing only the "worst" message. I'd vote for always showing the latest, and again leaving it to the Status column for concisely telling the user about the final state a resource ended up in. * Stop prepending "info: " to every stdout/stderr message. It adds no value, clutters up the display, and worsens horizontal usage. * Lessen the verbosity of update headline messages, so we now instead of e.g. "Previewing update of stack 'x':", we just say "Previewing update (x):". * Eliminate vertical whitespace in the Diagnostics section. Every independent console.out previously was separated by an entire newline, which made the section look cluttered to my eyes. These are just streams of logs, there's no reason for the extra newlines. * Colorize the resource headers in the Diagnostic section light blue. Note that this will change various test baselines, which I will update next. I didn't want those in the same commit.
2018-09-24 17:31:19 +02:00
return ""
case deploy.OpCreate:
return "create"
case deploy.OpUpdate:
return "update"
case deploy.OpDelete:
return "delete"
case deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced, deploy.OpReadReplacement,
deploy.OpDiscardReplaced:
return "replace"
case deploy.OpRead:
// nolint: goconst
return "read"
case deploy.OpRefresh:
return "refresh"
case deploy.OpReadDiscard:
return "discard"
case deploy.OpImport, deploy.OpImportReplacement:
return "import"
}
contract.Failf("Unrecognized resource step op: %v", step.Op)
return ""
}
func (display *ProgressDisplay) getStepOp(step engine.StepEventMetadata) deploy.StepOp {
op := step.Op
// We will commonly hear about replacements as an actual series of steps. i.e. 'create
// replacement', 'replace', 'delete original'. During the actual application of these steps we
// want to see these individual steps. However, both before we apply all of them, and after
// they're all done, we want to show this as a single conceptual 'replace'/'replaced' step.
//
// Note: in non-interactive mode we can show these all as individual steps. This only applies
// to interactive mode, where there is only one line shown per resource, and we want it to be as
// clear as possible
if display.isTerminal {
// During preview, show the steps for replacing as a single 'replace' plan.
// Once done, show the steps for replacing as a single 'replaced' step.
// During update, we'll show these individual steps.
if display.isPreview || display.done {
if op == deploy.OpCreateReplacement || op == deploy.OpDeleteReplaced || op == deploy.OpDiscardReplaced {
return deploy.OpReplace
}
}
}
return op
}
func (display *ProgressDisplay) getStepOpLabel(step engine.StepEventMetadata) string {
return display.getStepOp(step).Prefix() + colors.Reset
}
func (display *ProgressDisplay) getStepInProgressDescription(step engine.StepEventMetadata) string {
op := display.getStepOp(step)
if isRootStack(step) && op == deploy.OpSame {
// most of the time a stack is unchanged. in that case we just show it as "running->done".
// otherwise, we show what is actually happening to it.
return "running"
}
getDescription := func() string {
if display.isPreview {
return display.getPreviewText(step)
}
switch op {
case deploy.OpSame:
Make a smattering of CLI UX improvements Since I was digging around over the weekend after the change to move away from light black, and the impact it had on less important information showing more prominently than it used to, I took a step back and did a deeper tidying up of things. Another side goal of this exercise was to be a little more respectful of terminal width; when we could say things with fewer words, I did so. * Stylize the preview/update summary differently, so that it stands out as a section. Also highlight the total changes with bold -- it turns out this has a similar effect to the bright white colorization, just without the negative effects on e.g. white terminals. * Eliminate some verbosity in the phrasing of change summaries. * Make all heading sections stylized consistently. This includes the color (bright magenta) and the vertical spacing (always a newline separating headings). We were previously inconsistent on this (e.g., outputs were under "---outputs---"). Now the headings are: Previewing (etc), Diagnostics, Outputs, Resources, Duration, and Permalink. * Fix an issue where we'd parent things to "global" until the stack object later showed up. Now we'll simply mock up a stack resource. * Don't show messages like "no change" or "unchanged". Prior to the light black removal, these faded into the background of the terminal. Now they just clutter up the display. Similar to the elision of "*" for OpSames in a prior commit, just leave these out. Now anything that's written is actually a meaningful status for the user to note. * Don't show the "3 info messages," etc. summaries in the Info column while an update is ongoing. Instead, just show the latest line. This is more respectful of width -- I often find that the important messages scroll off the right of my screen before this change. For discussion: - I actually wonder if we should eliminate the summary altogether and always just show the latest line. Or even blank it out. The summary feels better suited for the Diagnostics section, and the Status concisely tells us how a resource's update ended up (failed, succeeded, etc). - Similarly, I question the idea of showing only the "worst" message. I'd vote for always showing the latest, and again leaving it to the Status column for concisely telling the user about the final state a resource ended up in. * Stop prepending "info: " to every stdout/stderr message. It adds no value, clutters up the display, and worsens horizontal usage. * Lessen the verbosity of update headline messages, so we now instead of e.g. "Previewing update of stack 'x':", we just say "Previewing update (x):". * Eliminate vertical whitespace in the Diagnostics section. Every independent console.out previously was separated by an entire newline, which made the section look cluttered to my eyes. These are just streams of logs, there's no reason for the extra newlines. * Colorize the resource headers in the Diagnostic section light blue. Note that this will change various test baselines, which I will update next. I didn't want those in the same commit.
2018-09-24 17:31:19 +02:00
return ""
case deploy.OpCreate:
return "creating"
case deploy.OpUpdate:
return "updating"
case deploy.OpDelete:
return "deleting"
case deploy.OpReplace:
return "replacing"
case deploy.OpCreateReplacement:
return "creating replacement"
case deploy.OpDeleteReplaced:
return "deleting original"
case deploy.OpRead:
return "reading"
case deploy.OpReadReplacement:
return "reading for replacement"
case deploy.OpRefresh:
return "refreshing"
case deploy.OpReadDiscard:
return "discarding"
case deploy.OpDiscardReplaced:
return "discarding original"
case deploy.OpImport:
return "importing"
case deploy.OpImportReplacement:
return "importing replacement"
}
contract.Failf("Unrecognized resource step op: %v", op)
return ""
}
return op.Color() + getDescription() + colors.Reset
}
func writeString(b io.StringWriter, s string) {
_, err := b.WriteString(s)
contract.IgnoreError(err)
}