github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/backend/display/progress.go (about) 1 // Copyright 2016-2018, Pulumi Corporation. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // nolint: goconst 16 package display 17 18 import ( 19 "bytes" 20 "fmt" 21 "io" 22 "os" 23 "runtime" 24 "sort" 25 "strings" 26 "time" 27 "unicode" 28 29 "github.com/pulumi/pulumi/pkg/v3/backend/display/internal/terminal" 30 "github.com/pulumi/pulumi/pkg/v3/engine" 31 "github.com/pulumi/pulumi/pkg/v3/resource/deploy" 32 "github.com/pulumi/pulumi/sdk/v3/go/common/apitype" 33 "github.com/pulumi/pulumi/sdk/v3/go/common/diag" 34 "github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors" 35 "github.com/pulumi/pulumi/sdk/v3/go/common/display" 36 "github.com/pulumi/pulumi/sdk/v3/go/common/resource" 37 "github.com/pulumi/pulumi/sdk/v3/go/common/tokens" 38 "github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil" 39 "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" 40 ) 41 42 // DiagInfo contains the bundle of diagnostic information for a single resource. 43 type DiagInfo struct { 44 ErrorCount, WarningCount, InfoCount, DebugCount int 45 46 // The very last diagnostic event we got for this resource (regardless of severity). We'll print 47 // this out in the non-interactive mode whenever we get new events. Importantly, we don't want 48 // to print out the most significant diagnostic, as that means a flurry of event swill cause us 49 // to keep printing out the most significant diagnostic over and over again. 50 LastDiag *engine.DiagEventPayload 51 52 // The last error we received. If we have an error, and we're in tree-view, we'll prefer to 53 // show this over the last non-error diag so that users know about something bad early on. 54 LastError *engine.DiagEventPayload 55 56 // All the diagnostic events we've heard about this resource. We'll print the last diagnostic 57 // in the status region while a resource is in progress. At the end we'll print out all 58 // diagnostics for a resource. 59 // 60 // Diagnostic events are bucketed by their associated stream ID (with 0 being the default 61 // stream). 62 StreamIDToDiagPayloads map[int32][]engine.DiagEventPayload 63 } 64 65 type progressRenderer interface { 66 io.Closer 67 68 tick(display *ProgressDisplay) 69 rowUpdated(display *ProgressDisplay, row Row) 70 systemMessage(display *ProgressDisplay, payload engine.StdoutEventPayload) 71 done(display *ProgressDisplay) 72 println(display *ProgressDisplay, line string) 73 } 74 75 // ProgressDisplay organizes all the information needed for a dynamically updated "progress" view of an update. 76 type ProgressDisplay struct { 77 opts Options 78 79 renderer progressRenderer 80 81 // action is the kind of action (preview, update, refresh, etc) being performed. 82 action apitype.UpdateKind 83 // stack is the stack this progress pertains to. 84 stack tokens.Name 85 // proj is the project this progress pertains to. 86 proj tokens.PackageName 87 88 // Whether or not we're previewing. We don't know what we are actually doing until 89 // we get the initial 'prelude' event. 90 // 91 // this flag is only used to adjust how we describe what's going on to the user. 92 // i.e. if we're previewing we say things like "Would update" instead of "Updating". 93 isPreview bool 94 95 // The urn of the stack. 96 stackUrn resource.URN 97 98 // Whether or not we've seen outputs for the stack yet. 99 seenStackOutputs bool 100 101 // The summary event from the engine. If we get this, we'll print this after all 102 // normal resource events are heard. That way we don't interfere with all the progress 103 // messages we're outputting for them. 104 summaryEventPayload *engine.SummaryEventPayload 105 106 // Any system events we've received. They will be printed at the bottom of all the status rows 107 systemEventPayloads []engine.StdoutEventPayload 108 109 // Used to record the order that rows are created in. That way, when we present in a tree, we 110 // can keep things ordered so they will not jump around. 111 displayOrderCounter int 112 113 // What tick we're currently on. Used to determine the number of ellipses to concat to 114 // a status message to help indicate that things are still working. 115 currentTick int 116 117 headerRow Row 118 resourceRows []ResourceRow 119 120 // A mapping from each resource URN we are told about to its current status. 121 eventUrnToResourceRow map[resource.URN]ResourceRow 122 123 // Remember if we're a terminal or not. In a terminal we get a little bit fancier. 124 // For example, we'll go back and update previous status messages to make sure things 125 // align. We don't need to do that in non-terminal situations. 126 isTerminal bool 127 128 // If all progress messages are done and we can print out the final display. 129 done bool 130 131 // The column that the suffix should be added to 132 suffixColumn int 133 134 // the list of suffixes to rotate through 135 suffixesArray []string 136 137 // Maps used so we can generate short IDs for resource urns. 138 urnToID map[resource.URN]string 139 140 // Structure that tracks the time taken to perform an action on a resource. 141 opStopwatch opStopwatch 142 } 143 144 type opStopwatch struct { 145 start map[resource.URN]time.Time 146 end map[resource.URN]time.Time 147 } 148 149 func newOpStopwatch() opStopwatch { 150 return opStopwatch{ 151 start: map[resource.URN]time.Time{}, 152 end: map[resource.URN]time.Time{}, 153 } 154 } 155 156 var ( 157 // policyPayloads is a collection of policy violation events for a single resource. 158 policyPayloads []engine.PolicyViolationEventPayload 159 ) 160 161 func camelCase(s string) string { 162 if len(s) == 0 { 163 return s 164 } 165 166 runes := []rune(s) 167 runes[0] = unicode.ToLower(runes[0]) 168 return string(runes) 169 } 170 171 func simplifyTypeName(typ tokens.Type) string { 172 typeString := string(typ) 173 174 components := strings.Split(typeString, ":") 175 if len(components) != 3 { 176 return typeString 177 } 178 pkg, module, name := components[0], components[1], components[2] 179 180 if len(name) == 0 { 181 return typeString 182 } 183 184 lastSlashInModule := strings.LastIndexByte(module, '/') 185 if lastSlashInModule == -1 { 186 return typeString 187 } 188 file := module[lastSlashInModule+1:] 189 190 if file != camelCase(name) { 191 return typeString 192 } 193 194 return fmt.Sprintf("%v:%v:%v", pkg, module[:lastSlashInModule], name) 195 } 196 197 // getEventUrn returns the resource URN associated with an event, or the empty URN if this is not an 198 // event that has a URN. If this is also a 'step' event, then this will return the step metadata as 199 // well. 200 func getEventUrnAndMetadata(event engine.Event) (resource.URN, *engine.StepEventMetadata) { 201 switch event.Type { 202 case engine.ResourcePreEvent: 203 payload := event.Payload().(engine.ResourcePreEventPayload) 204 return payload.Metadata.URN, &payload.Metadata 205 case engine.ResourceOutputsEvent: 206 payload := event.Payload().(engine.ResourceOutputsEventPayload) 207 return payload.Metadata.URN, &payload.Metadata 208 case engine.ResourceOperationFailed: 209 payload := event.Payload().(engine.ResourceOperationFailedPayload) 210 return payload.Metadata.URN, &payload.Metadata 211 case engine.DiagEvent: 212 return event.Payload().(engine.DiagEventPayload).URN, nil 213 case engine.PolicyViolationEvent: 214 return event.Payload().(engine.PolicyViolationEventPayload).ResourceURN, nil 215 default: 216 return "", nil 217 } 218 } 219 220 // ShowProgressEvents displays the engine events with docker's progress view. 221 func ShowProgressEvents(op string, action apitype.UpdateKind, stack tokens.Name, proj tokens.PackageName, 222 events <-chan engine.Event, done chan<- bool, opts Options, isPreview bool) { 223 224 stdin := opts.Stdin 225 if stdin == nil { 226 stdin = os.Stdin 227 } 228 stdout := opts.Stdout 229 if stdout == nil { 230 stdout = os.Stdout 231 } 232 stderr := opts.Stderr 233 if stderr == nil { 234 stderr = os.Stderr 235 } 236 237 isInteractive, term := opts.IsInteractive, opts.term 238 if isInteractive && term == nil { 239 raw := runtime.GOOS != "windows" 240 t, err := terminal.Open(stdin, stdout, raw) 241 if err != nil { 242 _, err = fmt.Fprintln(stderr, "Failed to open terminal; treating display as non-interactive (%w)", err) 243 contract.IgnoreError(err) 244 isInteractive = false 245 } else { 246 term = t 247 } 248 } 249 250 var renderer progressRenderer 251 if isInteractive { 252 renderer = newInteractiveRenderer(term, opts) 253 } else { 254 renderer = newNonInteractiveRenderer(stdout, op, opts) 255 } 256 257 display := &ProgressDisplay{ 258 action: action, 259 isPreview: isPreview, 260 isTerminal: isInteractive, 261 opts: opts, 262 renderer: renderer, 263 stack: stack, 264 proj: proj, 265 eventUrnToResourceRow: make(map[resource.URN]ResourceRow), 266 suffixColumn: int(statusColumn), 267 suffixesArray: []string{"", ".", "..", "..."}, 268 urnToID: make(map[resource.URN]string), 269 displayOrderCounter: 1, 270 opStopwatch: newOpStopwatch(), 271 } 272 273 ticker := time.NewTicker(1 * time.Second) 274 if opts.deterministicOutput { 275 ticker.Stop() 276 } 277 display.processEvents(ticker, events) 278 contract.IgnoreClose(display.renderer) 279 ticker.Stop() 280 281 // let our caller know we're done. 282 close(done) 283 } 284 285 func (display *ProgressDisplay) println(line string) { 286 display.renderer.println(display, line) 287 } 288 289 type treeNode struct { 290 row Row 291 292 colorizedColumns []string 293 colorizedSuffix string 294 295 childNodes []*treeNode 296 } 297 298 func (display *ProgressDisplay) getOrCreateTreeNode( 299 result *[]*treeNode, urn resource.URN, row ResourceRow, urnToTreeNode map[resource.URN]*treeNode) *treeNode { 300 301 node, has := urnToTreeNode[urn] 302 if has { 303 return node 304 } 305 306 node = &treeNode{ 307 row: row, 308 colorizedColumns: row.ColorizedColumns(), 309 colorizedSuffix: row.ColorizedSuffix(), 310 } 311 312 urnToTreeNode[urn] = node 313 314 // if it's the not the root item, attach it as a child node to an appropriate parent item. 315 if urn != "" && urn != display.stackUrn { 316 var parentURN resource.URN 317 318 res := row.Step().Res 319 if res != nil { 320 parentURN = res.Parent 321 } 322 323 parentRow, hasParentRow := display.eventUrnToResourceRow[parentURN] 324 325 if !hasParentRow { 326 // If we haven't heard about this node's parent, then just parent it to the stack. 327 // Note: getting the parent row for the stack-urn will always succeed as we ensure that 328 // such a row is always there in ensureHeaderAndStackRows 329 parentURN = display.stackUrn 330 parentRow = display.eventUrnToResourceRow[parentURN] 331 } 332 333 parentNode := display.getOrCreateTreeNode(result, parentURN, parentRow, urnToTreeNode) 334 parentNode.childNodes = append(parentNode.childNodes, node) 335 return node 336 } 337 338 *result = append(*result, node) 339 return node 340 } 341 342 func (display *ProgressDisplay) generateTreeNodes() []*treeNode { 343 result := []*treeNode{} 344 345 result = append(result, &treeNode{ 346 row: display.headerRow, 347 colorizedColumns: display.headerRow.ColorizedColumns(), 348 }) 349 350 urnToTreeNode := make(map[resource.URN]*treeNode) 351 for urn, row := range display.eventUrnToResourceRow { 352 display.getOrCreateTreeNode(&result, urn, row, urnToTreeNode) 353 } 354 355 return result 356 } 357 358 func (display *ProgressDisplay) addIndentations(treeNodes []*treeNode, isRoot bool, indentation string) { 359 childIndentation := indentation + "│ " 360 lastChildIndentation := indentation + " " 361 362 for i, node := range treeNodes { 363 isLast := i == len(treeNodes)-1 364 365 prefix := indentation 366 367 var nestedIndentation string 368 if !isRoot { 369 if isLast { 370 prefix += "└─ " 371 nestedIndentation = lastChildIndentation 372 } else { 373 prefix += "├─ " 374 nestedIndentation = childIndentation 375 } 376 } 377 378 node.colorizedColumns[typeColumn] = prefix + node.colorizedColumns[typeColumn] 379 display.addIndentations(node.childNodes, false /*isRoot*/, nestedIndentation) 380 } 381 } 382 383 func (display *ProgressDisplay) convertNodesToRows( 384 nodes []*treeNode, maxSuffixLength int, rows *[][]string, maxColumnLengths *[]int) { 385 386 for _, node := range nodes { 387 if len(*maxColumnLengths) == 0 { 388 *maxColumnLengths = make([]int, len(node.colorizedColumns)) 389 } 390 391 colorizedColumns := make([]string, len(node.colorizedColumns)) 392 393 for i, colorizedColumn := range node.colorizedColumns { 394 columnWidth := colors.MeasureColorizedString(colorizedColumn) 395 396 if i == display.suffixColumn { 397 columnWidth += maxSuffixLength 398 colorizedColumns[i] = colorizedColumn + node.colorizedSuffix 399 } else { 400 colorizedColumns[i] = colorizedColumn 401 } 402 403 if columnWidth > (*maxColumnLengths)[i] { 404 (*maxColumnLengths)[i] = columnWidth 405 } 406 } 407 408 *rows = append(*rows, colorizedColumns) 409 410 display.convertNodesToRows(node.childNodes, maxSuffixLength, rows, maxColumnLengths) 411 } 412 } 413 414 type sortable []*treeNode 415 416 func (sortable sortable) Len() int { 417 return len(sortable) 418 } 419 420 func (sortable sortable) Less(i, j int) bool { 421 return sortable[i].row.DisplayOrderIndex() < sortable[j].row.DisplayOrderIndex() 422 } 423 424 func (sortable sortable) Swap(i, j int) { 425 sortable[i], sortable[j] = sortable[j], sortable[i] 426 } 427 428 func sortNodes(nodes []*treeNode) { 429 sort.Sort(sortable(nodes)) 430 431 for _, node := range nodes { 432 childNodes := node.childNodes 433 sortNodes(childNodes) 434 node.childNodes = childNodes 435 } 436 } 437 438 func (display *ProgressDisplay) filterOutUnnecessaryNodesAndSetDisplayTimes(nodes []*treeNode) []*treeNode { 439 result := []*treeNode{} 440 441 for _, node := range nodes { 442 node.childNodes = display.filterOutUnnecessaryNodesAndSetDisplayTimes(node.childNodes) 443 444 if node.row.HideRowIfUnnecessary() && len(node.childNodes) == 0 { 445 continue 446 } 447 448 display.displayOrderCounter++ 449 node.row.SetDisplayOrderIndex(display.displayOrderCounter) 450 result = append(result, node) 451 } 452 453 return result 454 } 455 456 func removeInfoColumnIfUnneeded(rows [][]string) { 457 // If there have been no info messages, then don't print out the info column header. 458 for i := 1; i < len(rows); i++ { 459 row := rows[i] 460 if row[len(row)-1] != "" { 461 return 462 } 463 } 464 465 firstRow := rows[0] 466 firstRow[len(firstRow)-1] = "" 467 } 468 469 // Performs all the work at the end once we've heard about the last message from the engine. 470 // Specifically, this will update the status messages for any resources, and will also then 471 // print out all final diagnostics. and finally will print out the summary. 472 func (display *ProgressDisplay) processEndSteps() { 473 // Figure out the rows that are currently in progress. 474 var inProgressRows []ResourceRow 475 if !display.isTerminal { 476 for _, v := range display.eventUrnToResourceRow { 477 if !v.IsDone() { 478 inProgressRows = append(inProgressRows, v) 479 } 480 } 481 } 482 483 // Transition the display to the 'done' state. This will transitively cause all 484 // rows to become done. 485 display.done = true 486 487 // Now print out all those rows that were in progress. They will now be 'done' 488 // since the display was marked 'done'. 489 if !display.isTerminal { 490 for _, v := range inProgressRows { 491 display.renderer.rowUpdated(display, v) 492 } 493 } 494 495 // Now refresh everything. This ensures that we go back and remove things like the diagnostic 496 // messages from a status message (since we're going to print them all) below. Note, this will 497 // only do something in a terminal. This is what we want, because if we're not in a terminal we 498 // don't really want to reprint any finished items we've already printed. 499 display.renderer.done(display) 500 501 // Render several "sections" of output based on available data as applicable. 502 display.println("") 503 wroteDiagnosticHeader := display.printDiagnostics() 504 wrotePolicyViolations := display.printPolicyViolations() 505 display.printOutputs() 506 // If no policies violated, print policy packs applied. 507 if !wrotePolicyViolations { 508 display.printSummary(wroteDiagnosticHeader) 509 } 510 } 511 512 // printDiagnostics prints a new "Diagnostics:" section with all of the diagnostics grouped by 513 // resource. If no diagnostics were emitted, prints nothing. 514 func (display *ProgressDisplay) printDiagnostics() bool { 515 // Since we display diagnostic information eagerly, we need to keep track of the first 516 // time we wrote some output so we don't inadvertently print the header twice. 517 wroteDiagnosticHeader := false 518 for _, row := range display.eventUrnToResourceRow { 519 // The header for the diagnogistics grouped by resource, e.g. "aws:apigateway:RestApi (accountsApi):" 520 wroteResourceHeader := false 521 522 // Each row in the display corresponded with a resource, and that resource could have emitted 523 // diagnostics to various streams. 524 for id, payloads := range row.DiagInfo().StreamIDToDiagPayloads { 525 if len(payloads) == 0 { 526 continue 527 } 528 529 if id != 0 { 530 // For the non-default stream merge all the messages from the stream into a single 531 // message. 532 p := display.mergeStreamPayloadsToSinglePayload(payloads) 533 payloads = []engine.DiagEventPayload{p} 534 } 535 536 // Did we write any diagnostic information for the resource x stream? 537 wrote := false 538 for _, v := range payloads { 539 if v.Ephemeral { 540 continue 541 } 542 543 msg := display.renderProgressDiagEvent(v, true /*includePrefix:*/) 544 545 lines := splitIntoDisplayableLines(msg) 546 if len(lines) == 0 { 547 continue 548 } 549 550 // If we haven't printed the Diagnostics header, do so now. 551 if !wroteDiagnosticHeader { 552 wroteDiagnosticHeader = true 553 display.println(colors.SpecHeadline + "Diagnostics:" + colors.Reset) 554 } 555 // If we haven't printed the header for the resource, do so now. 556 if !wroteResourceHeader { 557 wroteResourceHeader = true 558 columns := row.ColorizedColumns() 559 display.println( 560 " " + colors.BrightBlue + columns[typeColumn] + " (" + columns[nameColumn] + "):" + colors.Reset) 561 } 562 563 for _, line := range lines { 564 line = strings.TrimRightFunc(line, unicode.IsSpace) 565 display.println(" " + line) 566 } 567 568 wrote = true 569 } 570 571 if wrote { 572 display.println("") 573 } 574 } 575 576 } 577 return wroteDiagnosticHeader 578 } 579 580 // printPolicyViolations prints a new "Policy Violation:" section with all of the violations 581 // grouped by policy pack. If no policy violations were encountered, prints nothing. 582 func (display *ProgressDisplay) printPolicyViolations() bool { 583 // Loop through every resource and gather up all policy violations encountered. 584 var policyEvents []engine.PolicyViolationEventPayload 585 for _, row := range display.eventUrnToResourceRow { 586 policyPayloads := row.PolicyPayloads() 587 if len(policyPayloads) == 0 { 588 continue 589 } 590 policyEvents = append(policyEvents, policyPayloads...) 591 } 592 if len(policyEvents) == 0 { 593 return false 594 } 595 // Sort policy events by: policy pack name, policy pack version, enforcement level, 596 // policy name, and finally the URN of the resource. 597 sort.SliceStable(policyEvents, func(i, j int) bool { 598 eventI, eventJ := policyEvents[i], policyEvents[j] 599 if packNameCmp := strings.Compare( 600 eventI.PolicyPackName, 601 eventJ.PolicyPackName); packNameCmp != 0 { 602 return packNameCmp < 0 603 } 604 if packVerCmp := strings.Compare( 605 eventI.PolicyPackVersion, 606 eventJ.PolicyPackVersion); packVerCmp != 0 { 607 return packVerCmp < 0 608 } 609 if enfLevelCmp := strings.Compare( 610 string(eventI.EnforcementLevel), 611 string(eventJ.EnforcementLevel)); enfLevelCmp != 0 { 612 return enfLevelCmp < 0 613 } 614 if policyNameCmp := strings.Compare( 615 eventI.PolicyName, 616 eventJ.PolicyName); policyNameCmp != 0 { 617 return policyNameCmp < 0 618 } 619 urnCmp := strings.Compare( 620 string(eventI.ResourceURN), 621 string(eventJ.ResourceURN)) 622 return urnCmp < 0 623 }) 624 625 // Print every policy violation, printing a new header when necessary. 626 display.println(display.opts.Color.Colorize(colors.SpecHeadline + "Policy Violations:" + colors.Reset)) 627 628 for _, policyEvent := range policyEvents { 629 // Print the individual policy event. 630 c := colors.SpecImportant 631 if policyEvent.EnforcementLevel == apitype.Mandatory { 632 c = colors.SpecError 633 } 634 635 policyNameLine := fmt.Sprintf(" %s[%s] %s v%s %s %s (%s: %s)", 636 c, policyEvent.EnforcementLevel, 637 policyEvent.PolicyPackName, 638 policyEvent.PolicyPackVersion, colors.Reset, 639 policyEvent.PolicyName, 640 policyEvent.ResourceURN.Type(), 641 policyEvent.ResourceURN.Name()) 642 display.println(policyNameLine) 643 644 // The message may span multiple lines, so we massage it so it will be indented properly. 645 message := strings.ReplaceAll(policyEvent.Message, "\n", "\n ") 646 messageLine := fmt.Sprintf(" %s", message) 647 display.println(messageLine) 648 } 649 return true 650 } 651 652 // printOutputs prints the Stack's outputs for the display in a new section, if appropriate. 653 func (display *ProgressDisplay) printOutputs() { 654 // Printing the stack's outputs wasn't desired. 655 if display.opts.SuppressOutputs { 656 return 657 } 658 // Cannot display outputs for the stack if we don't know its URN. 659 if display.stackUrn == "" { 660 return 661 } 662 663 stackStep := display.eventUrnToResourceRow[display.stackUrn].Step() 664 665 props := getResourceOutputsPropertiesString( 666 stackStep, 1, display.isPreview, display.opts.Debug, 667 false /* refresh */, display.opts.ShowSameResources) 668 if props != "" { 669 display.println(colors.SpecHeadline + "Outputs:" + colors.Reset) 670 display.println(props) 671 } 672 } 673 674 // printSummary prints the Stack's SummaryEvent in a new section if applicable. 675 func (display *ProgressDisplay) printSummary(wroteDiagnosticHeader bool) { 676 // If we never saw the SummaryEvent payload, we have nothing to do. 677 if display.summaryEventPayload == nil { 678 return 679 } 680 681 msg := renderSummaryEvent(*display.summaryEventPayload, wroteDiagnosticHeader, display.opts) 682 display.println(msg) 683 } 684 685 func (display *ProgressDisplay) mergeStreamPayloadsToSinglePayload( 686 payloads []engine.DiagEventPayload) engine.DiagEventPayload { 687 buf := bytes.Buffer{} 688 689 for _, p := range payloads { 690 buf.WriteString(display.renderProgressDiagEvent(p, false /*includePrefix:*/)) 691 } 692 693 firstPayload := payloads[0] 694 msg := buf.String() 695 return engine.DiagEventPayload{ 696 URN: firstPayload.URN, 697 Message: msg, 698 Prefix: firstPayload.Prefix, 699 Color: firstPayload.Color, 700 Severity: firstPayload.Severity, 701 StreamID: firstPayload.StreamID, 702 Ephemeral: firstPayload.Ephemeral, 703 } 704 } 705 706 func splitIntoDisplayableLines(msg string) []string { 707 lines := strings.Split(msg, "\n") 708 709 // Trim off any trailing blank lines in the message. 710 for len(lines) > 0 { 711 lastLine := lines[len(lines)-1] 712 if strings.TrimSpace(colors.Never.Colorize(lastLine)) == "" { 713 lines = lines[0 : len(lines)-1] 714 } else { 715 break 716 } 717 } 718 719 return lines 720 } 721 722 func (display *ProgressDisplay) processTick() { 723 // Got a tick. Update the progress display if we're in a terminal. If we're not, 724 // print a hearbeat message every 10 seconds after our last output so that the user 725 // knows something is going on. This is also helpful for hosts like jenkins that 726 // often timeout a process if output is not seen in a while. 727 display.currentTick++ 728 729 display.renderer.tick(display) 730 } 731 732 func (display *ProgressDisplay) getRowForURN(urn resource.URN, metadata *engine.StepEventMetadata) ResourceRow { 733 // If there's already a row for this URN, return it. 734 row, has := display.eventUrnToResourceRow[urn] 735 if has { 736 return row 737 } 738 739 // First time we're hearing about this resource. Create an initial nearly-empty status for it. 740 step := engine.StepEventMetadata{URN: urn, Op: deploy.OpSame} 741 if metadata != nil { 742 step = *metadata 743 } 744 745 // If this is the first time we're seeing an event for the stack resource, check to see if we've already 746 // recorded root events that we want to reassociate with this URN. 747 if isRootURN(urn) { 748 display.stackUrn = urn 749 750 if row, has = display.eventUrnToResourceRow[""]; has { 751 row.SetStep(step) 752 display.eventUrnToResourceRow[urn] = row 753 delete(display.eventUrnToResourceRow, "") 754 return row 755 } 756 } 757 758 row = &resourceRowData{ 759 display: display, 760 tick: display.currentTick, 761 diagInfo: &DiagInfo{}, 762 policyPayloads: policyPayloads, 763 step: step, 764 hideRowIfUnnecessary: true, 765 } 766 767 display.eventUrnToResourceRow[urn] = row 768 769 display.ensureHeaderAndStackRows() 770 display.resourceRows = append(display.resourceRows, row) 771 return row 772 } 773 774 func (display *ProgressDisplay) processNormalEvent(event engine.Event) { 775 switch event.Type { 776 case engine.PreludeEvent: 777 // A prelude event can just be printed out directly to the console. 778 // Note: we should probably make sure we don't get any prelude events 779 // once we start hearing about actual resource events. 780 payload := event.Payload().(engine.PreludeEventPayload) 781 preludeEventString := renderPreludeEvent(payload, display.opts) 782 if display.isTerminal { 783 display.processNormalEvent(engine.NewEvent(engine.DiagEvent, engine.DiagEventPayload{ 784 Ephemeral: false, 785 Severity: diag.Info, 786 Color: cmdutil.GetGlobalColorization(), 787 Message: preludeEventString, 788 })) 789 } else { 790 display.println(preludeEventString) 791 } 792 return 793 case engine.SummaryEvent: 794 // keep track of the summary event so that we can display it after all other 795 // resource-related events we receive. 796 payload := event.Payload().(engine.SummaryEventPayload) 797 display.summaryEventPayload = &payload 798 return 799 case engine.DiagEvent: 800 msg := display.renderProgressDiagEvent(event.Payload().(engine.DiagEventPayload), true /*includePrefix:*/) 801 if msg == "" { 802 return 803 } 804 case engine.StdoutColorEvent: 805 display.handleSystemEvent(event.Payload().(engine.StdoutEventPayload)) 806 return 807 } 808 809 // At this point, all events should relate to resources. 810 eventUrn, metadata := getEventUrnAndMetadata(event) 811 812 // If we're suppressing reads from the tree-view, then convert notifications about reads into 813 // ephemeral messages that will go into the info column. 814 if metadata != nil && !display.opts.ShowReads { 815 if metadata.Op == deploy.OpReadDiscard || metadata.Op == deploy.OpReadReplacement { 816 // just flat out ignore read discards/replace. They're only relevant in the context of 817 // 'reads', and we only present reads as an ephemeral diagnostic anyways. 818 return 819 } 820 821 if metadata.Op == deploy.OpRead { 822 // Don't show reads as operations on a specific resource. It's an underlying detail 823 // that we don't want to clutter up the display with. However, to help users know 824 // what's going on, we can show them as ephemeral diagnostic messages that are 825 // associated at the top level with the stack. That way if things are taking a while, 826 // there's insight in the display as to what's going on. 827 display.processNormalEvent(engine.NewEvent(engine.DiagEvent, engine.DiagEventPayload{ 828 Ephemeral: true, 829 Severity: diag.Info, 830 Color: cmdutil.GetGlobalColorization(), 831 Message: fmt.Sprintf("read %v %v", simplifyTypeName(eventUrn.Type()), eventUrn.Name()), 832 })) 833 return 834 } 835 } 836 837 if eventUrn == "" { 838 // If this event has no URN, associate it with the stack. Note that there may not yet be a stack resource, in 839 // which case this is a no-op. 840 eventUrn = display.stackUrn 841 } 842 isRootEvent := eventUrn == display.stackUrn 843 844 row := display.getRowForURN(eventUrn, metadata) 845 846 // Don't bother showing certain events (for example, things that are unchanged). However 847 // always show the root 'stack' resource so we can indicate that it's still running, and 848 // also so we have something to attach unparented diagnostic events to. 849 hideRowIfUnnecessary := metadata != nil && !shouldShow(*metadata, display.opts) && !isRootEvent 850 // Always show row if there's a policy violation event. Policy violations prevent resource 851 // registration, so if we don't show the row, the violation gets attributed to the stack 852 // resource rather than the resources whose policy failed. 853 hideRowIfUnnecessary = hideRowIfUnnecessary || event.Type == engine.PolicyViolationEvent 854 if !hideRowIfUnnecessary { 855 row.SetHideRowIfUnnecessary(false) 856 } 857 858 if event.Type == engine.ResourcePreEvent { 859 step := event.Payload().(engine.ResourcePreEventPayload).Metadata 860 861 // Register the resource update start time to calculate duration 862 // and time elapsed. 863 display.opStopwatch.start[step.URN] = time.Now() 864 865 row.SetStep(step) 866 } else if event.Type == engine.ResourceOutputsEvent { 867 isRefresh := display.getStepOp(row.Step()) == deploy.OpRefresh 868 step := event.Payload().(engine.ResourceOutputsEventPayload).Metadata 869 870 // Register the resource update end time to calculate duration 871 // to display. 872 display.opStopwatch.end[step.URN] = time.Now() 873 874 // Is this the stack outputs event? If so, we'll need to print it out at the end of the plan. 875 if step.URN == display.stackUrn { 876 display.seenStackOutputs = true 877 } 878 879 row.SetStep(step) 880 row.AddOutputStep(step) 881 882 // If we're not in a terminal, we may not want to display this row again: if we're displaying a preview or if 883 // this step is a no-op for a custom resource, refreshing this row will simply duplicate its earlier output. 884 hasMeaningfulOutput := isRefresh || 885 !display.isPreview && (step.Res == nil || step.Res.Custom && step.Op != deploy.OpSame) 886 if !display.isTerminal && !hasMeaningfulOutput { 887 return 888 } 889 } else if event.Type == engine.ResourceOperationFailed { 890 row.SetFailed() 891 } else if event.Type == engine.DiagEvent { 892 // also record this diagnostic so we print it at the end. 893 row.RecordDiagEvent(event) 894 } else if event.Type == engine.PolicyViolationEvent { 895 // also record this policy violation so we print it at the end. 896 row.RecordPolicyViolationEvent(event) 897 } else { 898 contract.Failf("Unhandled event type '%s'", event.Type) 899 } 900 901 display.renderer.rowUpdated(display, row) 902 } 903 904 func (display *ProgressDisplay) handleSystemEvent(payload engine.StdoutEventPayload) { 905 // Make sure we have a header to display 906 display.ensureHeaderAndStackRows() 907 908 display.systemEventPayloads = append(display.systemEventPayloads, payload) 909 910 display.renderer.systemMessage(display, payload) 911 } 912 913 func (display *ProgressDisplay) ensureHeaderAndStackRows() { 914 if display.headerRow == nil { 915 // about to make our first status message. make sure we present the header line first. 916 display.headerRow = &headerRowData{display: display} 917 } 918 919 // we've added at least one row to the table. make sure we have a row to designate the 920 // stack if we haven't already heard about it yet. This also ensures that as we build 921 // the tree we can always guarantee there's a 'root' to parent anything to. 922 _, hasStackRow := display.eventUrnToResourceRow[display.stackUrn] 923 if hasStackRow { 924 return 925 } 926 927 stackRow := &resourceRowData{ 928 display: display, 929 tick: display.currentTick, 930 diagInfo: &DiagInfo{}, 931 policyPayloads: policyPayloads, 932 step: engine.StepEventMetadata{Op: deploy.OpSame}, 933 hideRowIfUnnecessary: false, 934 } 935 936 display.eventUrnToResourceRow[display.stackUrn] = stackRow 937 display.resourceRows = append(display.resourceRows, stackRow) 938 } 939 940 func (display *ProgressDisplay) processEvents(ticker *time.Ticker, events <-chan engine.Event) { 941 // Main processing loop. The purpose of this func is to read in events from the engine 942 // and translate them into Status objects and progress messages to be presented to the 943 // command line. 944 for { 945 select { 946 case <-ticker.C: 947 display.processTick() 948 949 case event := <-events: 950 if event.Type == "" || event.Type == engine.CancelEvent { 951 // Engine finished sending events. Do all the final processing and return 952 // from this local func. This will print out things like full diagnostic 953 // events, as well as the summary event from the engine. 954 display.processEndSteps() 955 return 956 } 957 958 display.processNormalEvent(event) 959 } 960 } 961 } 962 963 func (display *ProgressDisplay) renderProgressDiagEvent(payload engine.DiagEventPayload, includePrefix bool) string { 964 if payload.Severity == diag.Debug && !display.opts.Debug { 965 return "" 966 } 967 968 msg := payload.Message 969 if includePrefix { 970 msg = payload.Prefix + msg 971 } 972 973 return strings.TrimRightFunc(msg, unicode.IsSpace) 974 } 975 976 func (display *ProgressDisplay) getStepDoneDescription(step engine.StepEventMetadata, failed bool) string { 977 makeError := func(v string) string { 978 return colors.SpecError + "**" + v + "**" + colors.Reset 979 } 980 981 op := display.getStepOp(step) 982 983 if display.isPreview { 984 // During a preview, when we transition to done, we'll print out summary text describing the step instead of a 985 // past-tense verb describing the step that was performed. 986 return deploy.Color(op) + display.getPreviewDoneText(step) + colors.Reset 987 } 988 989 getDescription := func() string { 990 opText := "" 991 if failed { 992 switch op { 993 case deploy.OpSame: 994 opText = "failed" 995 case deploy.OpCreate, deploy.OpCreateReplacement: 996 opText = "creating failed" 997 case deploy.OpUpdate: 998 opText = "updating failed" 999 case deploy.OpDelete, deploy.OpDeleteReplaced: 1000 opText = "deleting failed" 1001 case deploy.OpReplace: 1002 opText = "replacing failed" 1003 case deploy.OpRead, deploy.OpReadReplacement: 1004 opText = "reading failed" 1005 case deploy.OpRefresh: 1006 opText = "refreshing failed" 1007 case deploy.OpReadDiscard, deploy.OpDiscardReplaced: 1008 opText = "discarding failed" 1009 case deploy.OpImport, deploy.OpImportReplacement: 1010 opText = "importing failed" 1011 default: 1012 contract.Failf("Unrecognized resource step op: %v", op) 1013 return "" 1014 } 1015 } else { 1016 switch op { 1017 case deploy.OpSame: 1018 opText = "" 1019 case deploy.OpCreate: 1020 opText = "created" 1021 case deploy.OpUpdate: 1022 opText = "updated" 1023 case deploy.OpDelete: 1024 opText = "deleted" 1025 case deploy.OpReplace: 1026 opText = "replaced" 1027 case deploy.OpCreateReplacement: 1028 opText = "created replacement" 1029 case deploy.OpDeleteReplaced: 1030 opText = "deleted original" 1031 case deploy.OpRead: 1032 // nolint: goconst 1033 opText = "read" 1034 case deploy.OpReadReplacement: 1035 opText = "read for replacement" 1036 case deploy.OpRefresh: 1037 opText = "refresh" 1038 case deploy.OpReadDiscard: 1039 opText = "discarded" 1040 case deploy.OpDiscardReplaced: 1041 opText = "discarded original" 1042 case deploy.OpImport: 1043 opText = "imported" 1044 case deploy.OpImportReplacement: 1045 opText = "imported replacement" 1046 default: 1047 contract.Failf("Unrecognized resource step op: %v", op) 1048 return "" 1049 } 1050 } 1051 if op == deploy.OpSame || display.opts.deterministicOutput || display.opts.SuppressTimings { 1052 return opText 1053 } 1054 1055 start, ok := display.opStopwatch.start[step.URN] 1056 if !ok { 1057 return opText 1058 } 1059 1060 end, ok := display.opStopwatch.end[step.URN] 1061 if !ok { 1062 return opText 1063 } 1064 1065 opDuration := end.Sub(start).Seconds() 1066 if opDuration < 1 { 1067 // Display a more fine-grain duration as the operation 1068 // has completed. 1069 return fmt.Sprintf("%s (%.2fs)", opText, opDuration) 1070 } 1071 return fmt.Sprintf("%s (%ds)", opText, int(opDuration)) 1072 } 1073 1074 if failed { 1075 return makeError(getDescription()) 1076 } 1077 1078 return deploy.Color(op) + getDescription() + colors.Reset 1079 } 1080 1081 func (display *ProgressDisplay) getPreviewText(step engine.StepEventMetadata) string { 1082 switch step.Op { 1083 case deploy.OpSame: 1084 return "" 1085 case deploy.OpCreate: 1086 return "create" 1087 case deploy.OpUpdate: 1088 return "update" 1089 case deploy.OpDelete: 1090 return "delete" 1091 case deploy.OpReplace: 1092 return "replace" 1093 case deploy.OpCreateReplacement: 1094 return "create replacement" 1095 case deploy.OpDeleteReplaced: 1096 return "delete original" 1097 case deploy.OpRead: 1098 // nolint: goconst 1099 return "read" 1100 case deploy.OpReadReplacement: 1101 return "read for replacement" 1102 case deploy.OpRefresh: 1103 return "refreshing" 1104 case deploy.OpReadDiscard: 1105 return "discard" 1106 case deploy.OpDiscardReplaced: 1107 return "discard original" 1108 case deploy.OpImport: 1109 return "import" 1110 case deploy.OpImportReplacement: 1111 return "import replacement" 1112 } 1113 1114 contract.Failf("Unrecognized resource step op: %v", step.Op) 1115 return "" 1116 } 1117 1118 // getPreviewDoneText returns a textual representation for this step, suitable for display during a preview once the 1119 // preview has completed. 1120 func (display *ProgressDisplay) getPreviewDoneText(step engine.StepEventMetadata) string { 1121 switch step.Op { 1122 case deploy.OpSame: 1123 return "" 1124 case deploy.OpCreate: 1125 return "create" 1126 case deploy.OpUpdate: 1127 return "update" 1128 case deploy.OpDelete: 1129 return "delete" 1130 case deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced, deploy.OpReadReplacement, 1131 deploy.OpDiscardReplaced: 1132 return "replace" 1133 case deploy.OpRead: 1134 // nolint: goconst 1135 return "read" 1136 case deploy.OpRefresh: 1137 return "refresh" 1138 case deploy.OpReadDiscard: 1139 return "discard" 1140 case deploy.OpImport, deploy.OpImportReplacement: 1141 return "import" 1142 } 1143 1144 contract.Failf("Unrecognized resource step op: %v", step.Op) 1145 return "" 1146 } 1147 1148 func (display *ProgressDisplay) getStepOp(step engine.StepEventMetadata) display.StepOp { 1149 op := step.Op 1150 1151 // We will commonly hear about replacements as an actual series of steps. i.e. 'create 1152 // replacement', 'replace', 'delete original'. During the actual application of these steps we 1153 // want to see these individual steps. However, both before we apply all of them, and after 1154 // they're all done, we want to show this as a single conceptual 'replace'/'replaced' step. 1155 // 1156 // Note: in non-interactive mode we can show these all as individual steps. This only applies 1157 // to interactive mode, where there is only one line shown per resource, and we want it to be as 1158 // clear as possible 1159 if display.isTerminal { 1160 // During preview, show the steps for replacing as a single 'replace' plan. 1161 // Once done, show the steps for replacing as a single 'replaced' step. 1162 // During update, we'll show these individual steps. 1163 if display.isPreview || display.done { 1164 if op == deploy.OpCreateReplacement || op == deploy.OpDeleteReplaced || op == deploy.OpDiscardReplaced { 1165 return deploy.OpReplace 1166 } 1167 } 1168 } 1169 1170 return op 1171 } 1172 1173 func (display *ProgressDisplay) getStepOpLabel(step engine.StepEventMetadata, done bool) string { 1174 return deploy.Prefix(display.getStepOp(step), done) + colors.Reset 1175 } 1176 1177 func (display *ProgressDisplay) getStepInProgressDescription(step engine.StepEventMetadata) string { 1178 op := display.getStepOp(step) 1179 1180 if isRootStack(step) && op == deploy.OpSame { 1181 // most of the time a stack is unchanged. in that case we just show it as "running->done". 1182 // otherwise, we show what is actually happening to it. 1183 return "running" 1184 } 1185 1186 getDescription := func() string { 1187 if display.isPreview { 1188 return display.getPreviewText(step) 1189 } 1190 1191 opText := "" 1192 switch op { 1193 case deploy.OpSame: 1194 opText = "" 1195 case deploy.OpCreate: 1196 opText = "creating" 1197 case deploy.OpUpdate: 1198 opText = "updating" 1199 case deploy.OpDelete: 1200 opText = "deleting" 1201 case deploy.OpReplace: 1202 opText = "replacing" 1203 case deploy.OpCreateReplacement: 1204 opText = "creating replacement" 1205 case deploy.OpDeleteReplaced: 1206 opText = "deleting original" 1207 case deploy.OpRead: 1208 opText = "reading" 1209 case deploy.OpReadReplacement: 1210 opText = "reading for replacement" 1211 case deploy.OpRefresh: 1212 opText = "refreshing" 1213 case deploy.OpReadDiscard: 1214 opText = "discarding" 1215 case deploy.OpDiscardReplaced: 1216 opText = "discarding original" 1217 case deploy.OpImport: 1218 opText = "importing" 1219 case deploy.OpImportReplacement: 1220 opText = "importing replacement" 1221 default: 1222 contract.Failf("Unrecognized resource step op: %v", op) 1223 return "" 1224 } 1225 1226 if op == deploy.OpSame || display.opts.deterministicOutput || display.opts.SuppressTimings { 1227 return opText 1228 } 1229 1230 // Calculate operation time elapsed. 1231 start, ok := display.opStopwatch.start[step.URN] 1232 if !ok { 1233 return opText 1234 } 1235 1236 secondsElapsed := time.Now().Sub(start).Seconds() 1237 return fmt.Sprintf("%s (%ds)", opText, int(secondsElapsed)) 1238 1239 } 1240 return deploy.ColorProgress(op) + getDescription() + colors.Reset 1241 }