// Copyright 2016-2018, Pulumi Corporation. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // nolint: goconst package display import ( "bytes" "fmt" "io" "math" "os" "sort" "strings" "time" "unicode" "unicode/utf8" "github.com/docker/docker/pkg/term" "golang.org/x/crypto/ssh/terminal" "github.com/pulumi/pulumi/pkg/v3/engine" "github.com/pulumi/pulumi/pkg/v3/resource/deploy" "github.com/pulumi/pulumi/sdk/v3/go/common/apitype" "github.com/pulumi/pulumi/sdk/v3/go/common/diag" "github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors" "github.com/pulumi/pulumi/sdk/v3/go/common/resource" "github.com/pulumi/pulumi/sdk/v3/go/common/tokens" "github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil" "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" ) // Progress describes a message we want to show in the display. There are two types of messages, // simple 'Messages' which just get printed out as a single uninterpreted line, and 'Actions' which // are placed and updated in the progress-grid based on their ID. Messages do not need an ID, while // Actions must have an ID. type Progress struct { ID string Message string Action string } func makeMessageProgress(message string) Progress { return Progress{Message: message} } func makeActionProgress(id string, action string) Progress { contract.Assertf(id != "", "id must be non empty for action %s", action) contract.Assertf(action != "", "action must be non empty") return Progress{ID: id, Action: action} } // DiagInfo contains the bundle of diagnostic information for a single resource. type DiagInfo struct { ErrorCount, WarningCount, InfoCount, DebugCount int // The very last diagnostic event we got for this resource (regardless of severity). We'll print // this out in the non-interactive mode whenever we get new events. Importantly, we don't want // to print out the most significant diagnostic, as that means a flurry of event swill cause us // to keep printing out the most significant diagnostic over and over again. LastDiag *engine.DiagEventPayload // The last error we received. If we have an error, and we're in tree-view, we'll prefer to // show this over the last non-error diag so that users know about something bad early on. LastError *engine.DiagEventPayload // All the diagnostic events we've heard about this resource. We'll print the last diagnostic // in the status region while a resource is in progress. At the end we'll print out all // diagnostics for a resource. // // Diagnostic events are bucketed by their associated stream ID (with 0 being the default // stream). StreamIDToDiagPayloads map[int32][]engine.DiagEventPayload } // ProgressDisplay organizes all the information needed for a dynamically updated "progress" view of an update. 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 ( // policyPayloads is a collection of policy violation events for a single resource. policyPayloads []engine.PolicyViolationEventPayload ) func camelCase(s string) string { if len(s) == 0 { return s } runes := []rune(s) runes[0] = unicode.ToLower(runes[0]) return string(runes) } func simplifyTypeName(typ tokens.Type) string { typeString := string(typ) components := strings.Split(typeString, ":") if len(components) != 3 { return typeString } pkg, module, name := components[0], components[1], components[2] if len(name) == 0 { return typeString } lastSlashInModule := strings.LastIndexByte(module, '/') if lastSlashInModule == -1 { return typeString } file := module[lastSlashInModule+1:] if file != camelCase(name) { return typeString } return fmt.Sprintf("%v:%v:%v", pkg, module[:lastSlashInModule], name) } // getEventUrn returns the resource URN associated with an event, or the empty URN if this is not an // event that has a URN. If this is also a 'step' event, then this will return the step metadata as // well. func getEventUrnAndMetadata(event engine.Event) (resource.URN, *engine.StepEventMetadata) { switch event.Type { case engine.ResourcePreEvent: payload := event.Payload().(engine.ResourcePreEventPayload) return payload.Metadata.URN, &payload.Metadata case engine.ResourceOutputsEvent: payload := event.Payload().(engine.ResourceOutputsEventPayload) return payload.Metadata.URN, &payload.Metadata case engine.ResourceOperationFailed: payload := event.Payload().(engine.ResourceOperationFailedPayload) return payload.Metadata.URN, &payload.Metadata case engine.DiagEvent: return event.Payload().(engine.DiagEventPayload).URN, nil case engine.PolicyViolationEvent: return event.Payload().(engine.PolicyViolationEventPayload).ResourceURN, nil default: return "", nil } } // Converts the colorization tags in a progress message and then actually writes the progress // message to the output stream. This should be the only place in this file where we actually // process colorization tags. func (display *ProgressDisplay) colorizeAndWriteProgress(progress Progress) { if progress.Message != "" { progress.Message = display.opts.Color.Colorize(progress.Message) } if progress.Action != "" { progress.Action = display.opts.Color.Colorize(progress.Action) } if progress.ID != "" { // don't repeat the same output if there is no difference between the last time we // printed it and now. lastProgress, has := display.printedProgressCache[progress.ID] if has && lastProgress.Message == progress.Message && lastProgress.Action == progress.Action { return } display.printedProgressCache[progress.ID] = progress } if !display.isTerminal { // We're about to display something. Reset our spinner so that it will go on the next line. display.nonInteractiveSpinner.Reset() } display.progressOutput <- progress } func (display *ProgressDisplay) writeSimpleMessage(msg string) { display.colorizeAndWriteProgress(makeMessageProgress(msg)) } func (display *ProgressDisplay) writeBlankLine() { display.writeSimpleMessage(" ") } // 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) { stdout := opts.Stdout if stdout == nil { stdout = os.Stdout } stderr := opts.Stderr if stderr == nil { stderr = os.Stderr } // Create a ticker that will update all our status messages once a second. Any // in-flight resources will get a varying . .. ... ticker appended to them to // let the user know what is still being worked on. var spinner cmdutil.Spinner var ticker *time.Ticker if stdout == os.Stdout && stderr == os.Stderr && opts.IsInteractive { spinner, ticker = cmdutil.NewSpinnerAndTicker( fmt.Sprintf("%s%s...", cmdutil.EmojiOr("✨ ", "@ "), op), nil, 1 /*timesPerSecond*/) } else { spinner = &nopSpinner{} ticker = time.NewTicker(math.MaxInt64) } // 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, } // Assume we are not displaying in a terminal by default. display.isTerminal = false if stdout == os.Stdout { terminalWidth, terminalHeight, err := terminal.GetSize(int(os.Stdout.Fd())) if err == nil { // If the terminal has a size, use it. display.isTerminal = opts.IsInteractive display.terminalWidth = terminalWidth display.terminalHeight = terminalHeight // Don't bother attempting to treat this display as a terminal if it has no width/height. if display.isTerminal && (display.terminalWidth == 0 || display.terminalHeight == 0) { display.isTerminal = false _, err = fmt.Fprintln(stderr, "Treating display as non-terminal due to 0 width/height.") contract.IgnoreError(err) } // Fetch the canonical stdout stream, configured appropriately. _, stdout, _ = term.StdStreams() } } go func() { display.processEvents(ticker, events) // no more progress events from this point on. By closing the pipe, this will then cause // DisplayJSONMessagesToStream to finish once it processes the last message is receives from // pipeReader, causing DisplayEvents to finally complete. close(progressOutput) }() 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.Type(), policyEvent.ResourceURN.Name()) display.writeSimpleMessage(policyNameLine) // The message may span multiple lines, so we massage it so it will be indented properly. message := strings.ReplaceAll(policyEvent.Message, "\n", "\n ") messageLine := fmt.Sprintf(" %s", message) display.writeSimpleMessage(messageLine) } return true } // printOutputs prints the Stack's outputs for the display in a new section, if appropriate. func (display *ProgressDisplay) printOutputs() { // Printing the stack's outputs wasn't desired. if display.opts.SuppressOutputs { return } // Cannot display outputs for the stack if we don't know its URN. if display.stackUrn == "" { return } stackStep := display.eventUrnToResourceRow[display.stackUrn].Step() props := engine.GetResourceOutputsPropertiesString( stackStep, 1, display.isPreview, display.opts.Debug, false /* refresh */, display.opts.ShowSameResources) if props != "" { display.writeSimpleMessage(colors.SpecHeadline + "Outputs:" + colors.Reset) display.writeSimpleMessage(props) } } // printSummary prints the Stack's SummaryEvent in a new section if applicable. func (display *ProgressDisplay) printSummary(wroteDiagnosticHeader bool) { // If we never saw the SummaryEvent payload, we have nothing to do. if display.summaryEventPayload == nil { return } msg := renderSummaryEvent(display.action, *display.summaryEventPayload, wroteDiagnosticHeader, display.opts) display.writeSimpleMessage(msg) } func (display *ProgressDisplay) mergeStreamPayloadsToSinglePayload( payloads []engine.DiagEventPayload) engine.DiagEventPayload { buf := bytes.Buffer{} for _, p := range payloads { buf.WriteString(display.renderProgressDiagEvent(p, false /*includePrefix:*/)) } firstPayload := payloads[0] msg := buf.String() return engine.DiagEventPayload{ URN: firstPayload.URN, Message: msg, Prefix: firstPayload.Prefix, Color: firstPayload.Color, Severity: firstPayload.Severity, StreamID: firstPayload.StreamID, Ephemeral: firstPayload.Ephemeral, } } func splitIntoDisplayableLines(msg string) []string { lines := strings.Split(msg, "\n") // Trim off any trailing blank lines in the message. for len(lines) > 0 { lastLine := lines[len(lines)-1] if strings.TrimSpace(colors.Never.Colorize(lastLine)) == "" { lines = lines[0 : len(lines)-1] } else { break } } return lines } func (display *ProgressDisplay) processTick() { // Got a tick. Update the progress display if we're in a terminal. If we're not, // print a hearbeat message every 10 seconds after our last output so that the user // knows something is going on. This is also helpful for hosts like jenkins that // often timeout a process if output is not seen in a while. display.currentTick++ if display.isTerminal { display.refreshAllRowsIfInTerminal() } else { // Update the spinner to let the user know that that work is still happening. display.nonInteractiveSpinner.Tick() } } func (display *ProgressDisplay) getRowForURN(urn resource.URN, metadata *engine.StepEventMetadata) ResourceRow { // If there's already a row for this URN, return it. row, has := display.eventUrnToResourceRow[urn] if has { return row } // First time we're hearing about this resource. Create an initial nearly-empty status for it. step := engine.StepEventMetadata{URN: urn, Op: deploy.OpSame} if metadata != nil { step = *metadata } // If this is the first time we're seeing an event for the stack resource, check to see if we've already // recorded root events that we want to reassociate with this URN. if isRootURN(urn) { display.stackUrn = urn if row, has = display.eventUrnToResourceRow[""]; has { row.SetStep(step) display.eventUrnToResourceRow[urn] = row delete(display.eventUrnToResourceRow, "") return row } } row = &resourceRowData{ display: display, tick: display.currentTick, diagInfo: &DiagInfo{}, policyPayloads: policyPayloads, step: step, hideRowIfUnnecessary: true, } display.eventUrnToResourceRow[urn] = row display.ensureHeaderAndStackRows() display.resourceRows = append(display.resourceRows, row) return row } func (display *ProgressDisplay) processNormalEvent(event engine.Event) { switch event.Type { case engine.PreludeEvent: // A prelude event can just be printed out directly to the console. // Note: we should probably make sure we don't get any prelude events // once we start hearing about actual resource events. payload := event.Payload().(engine.PreludeEventPayload) preludeEventString := renderPreludeEvent(payload, display.opts) if display.isTerminal { display.processNormalEvent(engine.NewEvent(engine.DiagEvent, engine.DiagEventPayload{ Ephemeral: false, Severity: diag.Info, Color: cmdutil.GetGlobalColorization(), Message: preludeEventString, })) } else { display.writeSimpleMessage(preludeEventString) } return case engine.SummaryEvent: // keep track of the summary event so that we can display it after all other // resource-related events we receive. payload := event.Payload().(engine.SummaryEventPayload) display.summaryEventPayload = &payload return case engine.DiagEvent: msg := display.renderProgressDiagEvent(event.Payload().(engine.DiagEventPayload), true /*includePrefix:*/) if msg == "" { return } case engine.StdoutColorEvent: display.handleSystemEvent(event.Payload().(engine.StdoutEventPayload)) return } // At this point, all events should relate to resources. eventUrn, metadata := getEventUrnAndMetadata(event) // If we're suppressing reads from the tree-view, then convert notifications about reads into // ephemeral messages that will go into the info column. if metadata != nil && !display.opts.ShowReads { if metadata.Op == deploy.OpReadDiscard || metadata.Op == deploy.OpReadReplacement { // just flat out ignore read discards/replace. They're only relevant in the context of // 'reads', and we only present reads as an ephemeral diagnostic anyways. return } if metadata.Op == deploy.OpRead { // Don't show reads as operations on a specific resource. It's an underlying detail // that we don't want to clutter up the display with. However, to help users know // what's going on, we can show them as ephemeral diagnostic messages that are // associated at the top level with the stack. That way if things are taking a while, // there's insight in the display as to what's going on. display.processNormalEvent(engine.NewEvent(engine.DiagEvent, engine.DiagEventPayload{ Ephemeral: true, Severity: diag.Info, Color: cmdutil.GetGlobalColorization(), Message: fmt.Sprintf("read %v %v", simplifyTypeName(eventUrn.Type()), eventUrn.Name()), })) return } } if eventUrn == "" { // If this event has no URN, associate it with the stack. Note that there may not yet be a stack resource, in // which case this is a no-op. eventUrn = display.stackUrn } isRootEvent := eventUrn == display.stackUrn row := display.getRowForURN(eventUrn, metadata) // Don't bother showing certain events (for example, things that are unchanged). However // always show the root 'stack' resource so we can indicate that it's still running, and // also so we have something to attach unparented diagnostic events to. hideRowIfUnnecessary := metadata != nil && !shouldShow(*metadata, display.opts) && !isRootEvent // Always show row if there's a policy violation event. Policy violations prevent resource // registration, so if we don't show the row, the violation gets attributed to the stack // resource rather than the resources whose policy failed. hideRowIfUnnecessary = hideRowIfUnnecessary || event.Type == engine.PolicyViolationEvent if !hideRowIfUnnecessary { row.SetHideRowIfUnnecessary(false) } if event.Type == engine.ResourcePreEvent { step := event.Payload().(engine.ResourcePreEventPayload).Metadata row.SetStep(step) } else if event.Type == engine.ResourceOutputsEvent { isRefresh := display.getStepOp(row.Step()) == deploy.OpRefresh step := event.Payload().(engine.ResourceOutputsEventPayload).Metadata // Is this the stack outputs event? If so, we'll need to print it out at the end of the plan. if step.URN == display.stackUrn { display.seenStackOutputs = true } row.SetStep(step) row.AddOutputStep(step) // If we're not in a terminal, we may not want to display this row again: if we're displaying a preview or if // this step is a no-op for a custom resource, refreshing this row will simply duplicate its earlier output. hasMeaningfulOutput := isRefresh || !display.isPreview && (step.Res == nil || step.Res.Custom && step.Op != deploy.OpSame) if !display.isTerminal && !hasMeaningfulOutput { return } } else if event.Type == engine.ResourceOperationFailed { row.SetFailed() } else if event.Type == engine.DiagEvent { // also record this diagnostic so we print it at the end. row.RecordDiagEvent(event) } 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) }