* Make `async:true` the default for `invoke` calls (#3750) * Switch away from native grpc impl. (#3728) * Remove usage of the 'deasync' library from @pulumi/pulumi. (#3752) * Only retry as long as we get unavailable back. Anything else continues. (#3769) * Handle all errors for now. (#3781) * Do not assume --yes was present when using pulumi in non-interactive mode (#3793) * Upgrade all paths for sdk and pkg to v2 * Backport C# invoke classes and other recent gen changes (#4288) Adjust C# generation * Replace IDeployment with a sealed class (#4318) Replace IDeployment with a sealed class * .NET: default to args subtype rather than Args.Empty (#4320) * Adding system namespace for Dotnet code gen This is required for using Obsolute attributes for deprecations ``` Iam/InstanceProfile.cs(142,10): error CS0246: The type or namespace name 'ObsoleteAttribute' could not be found (are you missing a using directive or an assembly reference?) [/Users/stack72/code/go/src/github.com/pulumi/pulumi-aws/sdk/dotnet/Pulumi.Aws.csproj] Iam/InstanceProfile.cs(142,10): error CS0246: The type or namespace name 'Obsolete' could not be found (are you missing a using directive or an assembly reference?) [/Users/stack72/code/go/src/github.com/pulumi/pulumi-aws/sdk/dotnet/Pulumi.Aws.csproj] ``` * Fix the nullability of config type properties in C# codegen (#4379)
1401 lines
45 KiB
Go
1401 lines
45 KiB
Go
// 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"
|
|
"os"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"github.com/docker/docker/pkg/term"
|
|
"golang.org/x/crypto/ssh/terminal"
|
|
|
|
"github.com/pulumi/pulumi/pkg/v2/engine"
|
|
"github.com/pulumi/pulumi/pkg/v2/resource/deploy"
|
|
"github.com/pulumi/pulumi/sdk/v2/go/common/apitype"
|
|
"github.com/pulumi/pulumi/sdk/v2/go/common/diag"
|
|
"github.com/pulumi/pulumi/sdk/v2/go/common/diag/colors"
|
|
"github.com/pulumi/pulumi/sdk/v2/go/common/resource"
|
|
"github.com/pulumi/pulumi/sdk/v2/go/common/tokens"
|
|
"github.com/pulumi/pulumi/sdk/v2/go/common/util/cmdutil"
|
|
"github.com/pulumi/pulumi/sdk/v2/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.
|
|
type ProgressDisplay struct {
|
|
opts Options
|
|
progressOutput chan<- Progress
|
|
|
|
// action is the kind of action (preview, update, refresh, etc) being performed.
|
|
action apitype.UpdateKind
|
|
// stack is the stack this progress pertains to.
|
|
stack tokens.QName
|
|
// proj is the project this progress pertains to.
|
|
proj tokens.PackageName
|
|
|
|
// 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
|
|
|
|
// 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
|
|
|
|
// 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
|
|
|
|
// 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
|
|
|
|
headerRow Row
|
|
resourceRows []ResourceRow
|
|
|
|
// 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
|
|
|
|
// If all progress messages are done and we can print out the final display.
|
|
done bool
|
|
|
|
// 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
|
|
}
|
|
|
|
var (
|
|
// simple regex to take our names like "aws:function:Function" and convert to
|
|
// "aws:Function"
|
|
typeNameRegex = regexp.MustCompile("^(.*):(.*)/(.*):(.*)$")
|
|
// policyPayloads is a collection of policy violation events for a single resource.
|
|
policyPayloads []engine.PolicyViolationEventPayload
|
|
)
|
|
|
|
func simplifyTypeName(typ tokens.Type) string {
|
|
typeString := string(typ)
|
|
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
|
|
// 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) {
|
|
if event.Type == engine.ResourcePreEvent {
|
|
payload := event.Payload.(engine.ResourcePreEventPayload)
|
|
return payload.Metadata.URN, &payload.Metadata
|
|
} else if event.Type == engine.ResourceOutputsEvent {
|
|
payload := event.Payload.(engine.ResourceOutputsEventPayload)
|
|
return payload.Metadata.URN, &payload.Metadata
|
|
} else if event.Type == engine.ResourceOperationFailed {
|
|
payload := event.Payload.(engine.ResourceOperationFailedPayload)
|
|
return payload.Metadata.URN, &payload.Metadata
|
|
} else if event.Type == engine.DiagEvent {
|
|
return event.Payload.(engine.DiagEventPayload).URN, nil
|
|
} else if event.Type == engine.PolicyViolationEvent {
|
|
return event.Payload.(engine.PolicyViolationEventPayload).ResourceURN, nil
|
|
}
|
|
|
|
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(" ")
|
|
}
|
|
|
|
// ShowProgressEvents displays the engine events with docker's progress view.
|
|
func ShowProgressEvents(op string, action apitype.UpdateKind, stack tokens.QName, proj tokens.PackageName,
|
|
events <-chan engine.Event, done chan<- bool, opts Options, isPreview bool) {
|
|
// 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.
|
|
spinner, ticker := cmdutil.NewSpinnerAndTicker(
|
|
fmt.Sprintf("%s%s...", cmdutil.EmojiOr("✨ ", "@ "), op),
|
|
nil, 1 /*timesPerSecond*/)
|
|
|
|
// 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,
|
|
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,
|
|
}
|
|
|
|
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
|
|
} else {
|
|
// Else assume we are not displaying in a terminal.
|
|
display.isTerminal = false
|
|
}
|
|
|
|
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)
|
|
}()
|
|
|
|
_, stdout, _ := term.StdStreams()
|
|
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.
|
|
func (display *ProgressDisplay) getMessagePadding(
|
|
uncolorizedColumns []string, columnIndex int, maxColumnLengths []int) string {
|
|
|
|
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 := ""
|
|
|
|
for i := 0; i < len(colorizedColumns); i++ {
|
|
padding := display.getMessagePadding(uncolorizedColumns, i-1, maxColumnLengths)
|
|
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)
|
|
}
|
|
|
|
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 {
|
|
uncolorizedColumns := make([]string, len(columns))
|
|
|
|
for i, v := range columns {
|
|
uncolorizedColumns[i] = display.uncolorizeString(v)
|
|
}
|
|
|
|
return uncolorizedColumns
|
|
}
|
|
|
|
func (display *ProgressDisplay) refreshSingleRow(id string, row Row, maxColumnLengths []int) {
|
|
colorizedColumns := row.ColorizedColumns()
|
|
colorizedColumns[display.suffixColumn] += row.ColorizedSuffix()
|
|
display.refreshColumns(id, colorizedColumns, maxColumnLengths)
|
|
}
|
|
|
|
func (display *ProgressDisplay) refreshColumns(
|
|
id string, colorizedColumns []string, maxColumnLengths []int) {
|
|
|
|
uncolorizedColumns := display.uncolorizeColumns(colorizedColumns)
|
|
|
|
msg := display.getPaddedMessage(colorizedColumns, uncolorizedColumns, maxColumnLengths)
|
|
|
|
if display.isTerminal {
|
|
display.colorizeAndWriteProgress(makeActionProgress(id, msg))
|
|
} 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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (display *ProgressDisplay) refreshAllRowsIfInTerminal() {
|
|
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)
|
|
|
|
removeInfoColumnIfUnneeded(rows)
|
|
|
|
for i, row := range rows {
|
|
display.refreshColumns(fmt.Sprintf("%v", i), row, maxColumnLengths)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
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.Name(),
|
|
policyEvent.ResourceURN.Type())
|
|
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.Event{
|
|
Type: engine.DiagEvent,
|
|
Payload: 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.Event{
|
|
Type: engine.DiagEvent,
|
|
Payload: 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)
|
|
} 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() {
|
|
if display.headerRow == nil {
|
|
// about to make our first status message. make sure we present the header line first.
|
|
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:
|
|
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:
|
|
return ""
|
|
case deploy.OpCreate:
|
|
return "create"
|
|
case deploy.OpUpdate:
|
|
return "update"
|
|
case deploy.OpDelete:
|
|
return "delete"
|
|
case deploy.OpReplace:
|
|
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:
|
|
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:
|
|
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)
|
|
}
|