e1a52693dc
A resource can be imported by setting the `import` property in the resource options bag when instantiating a resource. In order to successfully import a resource, its desired configuration (i.e. its inputs) must not differ from its actual configuration (i.e. its state) as calculated by the resource's provider. There are a few interesting state transitions hiding here when importing a resource: 1. No prior resource exists in the checkpoint file. In this case, the resource is simply imported. 2. An external resource exists in the checkpoint file. In this case, the resource is imported and the old external state is discarded. 3. A non-external resource exists in the checkpoint file and its ID is different from the ID to import. In this case, the new resource is imported and the old resource is deleted. 4. A non-external resource exists in the checkpoint file, but the ID is the same as the ID to import. In this case, the import ID is ignored and the resource is treated as it would be in all cases except for changes that would replace the resource. In that case, the step generator issues an error that indicates that the import ID should be removed: were we to move forward with the replace, the new state of the stack would fall under case (3), which is almost certainly not what the user intends. Fixes #1662.
1278 lines
40 KiB
Go
1278 lines
40 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/apitype"
|
|
"github.com/pulumi/pulumi/pkg/diag"
|
|
"github.com/pulumi/pulumi/pkg/diag/colors"
|
|
"github.com/pulumi/pulumi/pkg/engine"
|
|
"github.com/pulumi/pulumi/pkg/resource"
|
|
"github.com/pulumi/pulumi/pkg/resource/deploy"
|
|
"github.com/pulumi/pulumi/pkg/tokens"
|
|
"github.com/pulumi/pulumi/pkg/util/cmdutil"
|
|
"github.com/pulumi/pulumi/pkg/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}
|
|
}
|
|
|
|
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 event of each severity kind. We'll print out the most significant of these (in the
|
|
// tree-view) next to a resource while it is in progress.
|
|
LastError, LastWarning, LastInfoError, LastInfo, LastDebug *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
|
|
}
|
|
|
|
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 of the terminal. Used so we can trim resource messages that are too long.
|
|
terminalWidth 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("^(.*):(.*)/(.*):(.*)$")
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
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, _, err := terminal.GetSize(int(os.Stdout.Fd()))
|
|
contract.IgnoreError(err)
|
|
display.isTerminal = opts.IsInteractive
|
|
display.terminalWidth = terminalWidth
|
|
|
|
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 := 1
|
|
|
|
// In the terminal we try to align the status messages for each resource.
|
|
// do not bother with this in the non-terminal case.
|
|
if columnIndex >= 0 && display.isTerminal {
|
|
column := uncolorizedColumns[columnIndex]
|
|
maxLength := maxColumnLengths[columnIndex]
|
|
|
|
extraWhitespace = maxLength - utf8.RuneCountInString(column)
|
|
contract.Assertf(extraWhitespace >= 0, "Neg whitespace. %v %s", maxLength, column)
|
|
|
|
// Place two spaces between all columns (except after the first column). The first
|
|
// column already has a ": " so it doesn't need the extra space.
|
|
if columnIndex >= 0 {
|
|
extraWhitespace += 2
|
|
}
|
|
}
|
|
|
|
return strings.Repeat(" ", 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) updateTerminalWidth() {
|
|
// don't do any refreshing if we're not in a terminal
|
|
if display.isTerminal {
|
|
currentTerminalWidth, _, err := terminal.GetSize(int(os.Stdout.Fd()))
|
|
contract.IgnoreError(err)
|
|
|
|
if currentTerminalWidth != display.terminalWidth {
|
|
display.terminalWidth = currentTerminalWidth
|
|
|
|
// 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.updateTerminalWidth()
|
|
|
|
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 i what we want, because if we're not in a terminal we
|
|
// don't really want to reprint any finished items we've already printed.
|
|
display.refreshAllRowsIfInTerminal()
|
|
|
|
// Print all diagnostics we've seen.
|
|
|
|
wroteDiagnosticHeader := false
|
|
|
|
for _, row := range display.eventUrnToResourceRow {
|
|
wroteResourceHeader := false
|
|
|
|
for id, payloads := range row.DiagInfo().StreamIDToDiagPayloads {
|
|
if len(payloads) > 0 {
|
|
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}
|
|
}
|
|
|
|
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 !wroteDiagnosticHeader {
|
|
wroteDiagnosticHeader = true
|
|
display.writeBlankLine()
|
|
display.writeSimpleMessage(
|
|
display.opts.Color.Colorize(colors.SpecHeadline + "Diagnostics:" + colors.Reset))
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we get stack outputs, display them at the end.
|
|
var wroteOutputs bool
|
|
if display.stackUrn != "" && display.seenStackOutputs && !display.opts.SuppressOutputs {
|
|
stackStep := display.eventUrnToResourceRow[display.stackUrn].Step()
|
|
props := engine.GetResourceOutputsPropertiesString(
|
|
stackStep, 1, display.isPreview, display.opts.Debug, false /* refresh */)
|
|
if props != "" {
|
|
if !wroteDiagnosticHeader {
|
|
display.writeBlankLine()
|
|
}
|
|
|
|
wroteOutputs = true
|
|
display.writeSimpleMessage(colors.SpecHeadline + "Outputs:" + colors.Reset)
|
|
display.writeSimpleMessage(props)
|
|
}
|
|
}
|
|
|
|
// print the summary
|
|
if display.summaryEventPayload != nil {
|
|
if !wroteDiagnosticHeader && !wroteOutputs {
|
|
display.writeBlankLine()
|
|
}
|
|
|
|
msg := renderSummaryEvent(display.action, *display.summaryEventPayload, 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{},
|
|
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)
|
|
display.writeSimpleMessage(renderPreludeEvent(payload, display.opts))
|
|
return
|
|
case engine.SummaryEvent:
|
|
// keep track of the summar 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 metadata != nil {
|
|
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
|
|
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{},
|
|
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)
|
|
}
|