github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/backend/display/rows.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 package display 16 17 import ( 18 "bytes" 19 "fmt" 20 "io" 21 "sort" 22 "strings" 23 24 "github.com/dustin/go-humanize/english" 25 "github.com/pulumi/pulumi/pkg/v3/engine" 26 "github.com/pulumi/pulumi/pkg/v3/resource/deploy" 27 "github.com/pulumi/pulumi/sdk/v3/go/common/apitype" 28 "github.com/pulumi/pulumi/sdk/v3/go/common/diag" 29 "github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors" 30 "github.com/pulumi/pulumi/sdk/v3/go/common/display" 31 "github.com/pulumi/pulumi/sdk/v3/go/common/resource" 32 ) 33 34 type Row interface { 35 DisplayOrderIndex() int 36 SetDisplayOrderIndex(index int) 37 38 ColorizedColumns() []string 39 ColorizedSuffix() string 40 41 HideRowIfUnnecessary() bool 42 SetHideRowIfUnnecessary(value bool) 43 } 44 45 type ResourceRow interface { 46 Row 47 48 Step() engine.StepEventMetadata 49 SetStep(step engine.StepEventMetadata) 50 AddOutputStep(step engine.StepEventMetadata) 51 52 // The tick we were on when we created this row. Purely used for generating an 53 // ellipses to show progress for in-flight resources. 54 Tick() int 55 56 IsDone() bool 57 58 SetFailed() 59 60 DiagInfo() *DiagInfo 61 PolicyPayloads() []engine.PolicyViolationEventPayload 62 63 RecordDiagEvent(diagEvent engine.Event) 64 RecordPolicyViolationEvent(diagEvent engine.Event) 65 } 66 67 // Implementation of a Row, used for the header of the grid. 68 type headerRowData struct { 69 display *ProgressDisplay 70 columns []string 71 } 72 73 func (data *headerRowData) HideRowIfUnnecessary() bool { 74 return false 75 } 76 77 func (data *headerRowData) SetHideRowIfUnnecessary(value bool) { 78 } 79 80 func (data *headerRowData) DisplayOrderIndex() int { 81 // sort the header before all other rows 82 return -1 83 } 84 85 func (data *headerRowData) SetDisplayOrderIndex(time int) { 86 // Nothing to do here. Header is always at the same index. 87 } 88 89 func (data *headerRowData) ColorizedColumns() []string { 90 if len(data.columns) == 0 { 91 header := func(msg string) string { 92 return columnHeader(msg) 93 } 94 95 var statusColumn string 96 if data.display.isPreview { 97 statusColumn = header("Plan") 98 } else { 99 statusColumn = header("Status") 100 } 101 data.columns = []string{"", header("Type"), header("Name"), statusColumn, header("Info")} 102 } 103 104 return data.columns 105 } 106 107 func (data *headerRowData) ColorizedSuffix() string { 108 return "" 109 } 110 111 // resourceRowData is the implementation of a row used for all the resource rows in the grid. 112 type resourceRowData struct { 113 displayOrderIndex int 114 115 display *ProgressDisplay 116 117 // The change that the engine wants apply to that resource. 118 step engine.StepEventMetadata 119 outputSteps []engine.StepEventMetadata 120 121 // The tick we were on when we created this row. Purely used for generating an 122 // ellipses to show progress for in-flight resources. 123 tick int 124 125 // If we failed this operation for any reason. 126 failed bool 127 128 diagInfo *DiagInfo 129 policyPayloads []engine.PolicyViolationEventPayload 130 131 // If this row should be hidden by default. We will hide unless we have any child nodes 132 // we need to show. 133 hideRowIfUnnecessary bool 134 } 135 136 func (data *resourceRowData) DisplayOrderIndex() int { 137 // sort the header before all other rows 138 return data.displayOrderIndex 139 } 140 141 func (data *resourceRowData) SetDisplayOrderIndex(index int) { 142 // only set this if it's the first time. 143 if data.displayOrderIndex == 0 { 144 data.displayOrderIndex = index 145 } 146 } 147 148 func (data *resourceRowData) HideRowIfUnnecessary() bool { 149 return data.hideRowIfUnnecessary 150 } 151 152 func (data *resourceRowData) SetHideRowIfUnnecessary(value bool) { 153 data.hideRowIfUnnecessary = value 154 } 155 156 func (data *resourceRowData) Step() engine.StepEventMetadata { 157 return data.step 158 } 159 160 func (data *resourceRowData) SetStep(step engine.StepEventMetadata) { 161 data.step = step 162 } 163 164 func (data *resourceRowData) AddOutputStep(step engine.StepEventMetadata) { 165 data.outputSteps = append(data.outputSteps, step) 166 } 167 168 func (data *resourceRowData) Tick() int { 169 return data.tick 170 } 171 172 func (data *resourceRowData) Failed() bool { 173 return data.failed 174 } 175 176 func (data *resourceRowData) SetFailed() { 177 data.failed = true 178 } 179 180 func (data *resourceRowData) DiagInfo() *DiagInfo { 181 return data.diagInfo 182 } 183 184 func (data *resourceRowData) RecordDiagEvent(event engine.Event) { 185 payload := event.Payload().(engine.DiagEventPayload) 186 data.recordDiagEventPayload(payload) 187 } 188 189 func (data *resourceRowData) recordDiagEventPayload(payload engine.DiagEventPayload) { 190 diagInfo := data.diagInfo 191 diagInfo.LastDiag = &payload 192 193 if payload.Severity == diag.Error { 194 diagInfo.LastError = &payload 195 } 196 197 if diagInfo.StreamIDToDiagPayloads == nil { 198 diagInfo.StreamIDToDiagPayloads = make(map[int32][]engine.DiagEventPayload) 199 } 200 201 payloads := diagInfo.StreamIDToDiagPayloads[payload.StreamID] 202 payloads = append(payloads, payload) 203 diagInfo.StreamIDToDiagPayloads[payload.StreamID] = payloads 204 205 if !payload.Ephemeral { 206 switch payload.Severity { 207 case diag.Error: 208 diagInfo.ErrorCount++ 209 case diag.Warning: 210 diagInfo.WarningCount++ 211 case diag.Infoerr: 212 diagInfo.InfoCount++ 213 case diag.Info: 214 diagInfo.InfoCount++ 215 case diag.Debug: 216 diagInfo.DebugCount++ 217 } 218 } 219 } 220 221 // PolicyInfo returns the PolicyInfo object associated with the resourceRowData. 222 func (data *resourceRowData) PolicyPayloads() []engine.PolicyViolationEventPayload { 223 return data.policyPayloads 224 } 225 226 // RecordPolicyViolationEvent records a policy event with the resourceRowData. 227 func (data *resourceRowData) RecordPolicyViolationEvent(event engine.Event) { 228 pePayload := event.Payload().(engine.PolicyViolationEventPayload) 229 data.policyPayloads = append(data.policyPayloads, pePayload) 230 } 231 232 type column int 233 234 const ( 235 opColumn column = 0 236 typeColumn column = 1 237 nameColumn column = 2 238 statusColumn column = 3 239 infoColumn column = 4 240 ) 241 242 func (data *resourceRowData) IsDone() bool { 243 if data.failed { 244 // consider a failed resource 'done'. 245 return true 246 } 247 248 if data.display.done { 249 // if the display is done, then we're definitely done. 250 return true 251 } 252 253 if isRootStack(data.step) { 254 // the root stack only becomes 'done' once the program has completed (i.e. the condition 255 // checked just above this). If the program is not finished, then always show the root 256 // stack as not done so the user sees "running..." presented for it. 257 return false 258 } 259 260 // We're done if we have the output-step for whatever step operation we're performing 261 return data.ContainsOutputsStep(data.step.Op) 262 } 263 264 func (data *resourceRowData) ContainsOutputsStep(op display.StepOp) bool { 265 for _, s := range data.outputSteps { 266 if s.Op == op { 267 return true 268 } 269 } 270 271 return false 272 } 273 274 func (data *resourceRowData) ColorizedSuffix() string { 275 if !data.IsDone() && data.display.isTerminal { 276 op := data.display.getStepOp(data.step) 277 if op != deploy.OpSame || isRootURN(data.step.URN) { 278 suffixes := data.display.suffixesArray 279 ellipses := suffixes[(data.tick+data.display.currentTick)%len(suffixes)] 280 281 return deploy.ColorProgress(op) + ellipses + colors.Reset 282 } 283 } 284 285 return "" 286 } 287 288 func (data *resourceRowData) ColorizedColumns() []string { 289 step := data.step 290 291 urn := data.step.URN 292 if urn == "" { 293 // If we don't have a URN yet, mock parent it to the global stack. 294 urn = resource.DefaultRootStackURN(data.display.stack.Q(), data.display.proj) 295 } 296 name := string(urn.Name()) 297 typ := simplifyTypeName(urn.Type()) 298 299 done := data.IsDone() 300 301 columns := make([]string, 5) 302 columns[opColumn] = data.display.getStepOpLabel(step, done) 303 columns[typeColumn] = typ 304 columns[nameColumn] = name 305 306 diagInfo := data.diagInfo 307 308 if done { 309 failed := data.failed || diagInfo.ErrorCount > 0 310 columns[statusColumn] = data.display.getStepDoneDescription(step, failed) 311 } else { 312 columns[statusColumn] = data.display.getStepInProgressDescription(step) 313 } 314 315 columns[infoColumn] = data.getInfoColumn() 316 return columns 317 } 318 319 func (data *resourceRowData) getInfoColumn() string { 320 step := data.step 321 switch step.Op { 322 case deploy.OpCreateReplacement, deploy.OpDeleteReplaced: 323 // if we're doing a replacement, see if we can find a replace step that contains useful 324 // information to display. 325 for _, outputStep := range data.outputSteps { 326 if outputStep.Op == deploy.OpReplace { 327 step = outputStep 328 } 329 } 330 331 case deploy.OpImport, deploy.OpImportReplacement: 332 // If we're doing an import, see if we have the imported state to diff. 333 for _, outputStep := range data.outputSteps { 334 if outputStep.Op == step.Op { 335 step = outputStep 336 } 337 } 338 } 339 340 var diagMsg string 341 appendDiagMessage := func(msg string) { 342 if diagMsg != "" { 343 diagMsg += "; " 344 } 345 346 diagMsg += msg 347 } 348 349 changes := getDiffInfo(step, data.display.action) 350 if colors.Never.Colorize(changes) != "" { 351 appendDiagMessage("[" + changes + "]") 352 } 353 354 diagInfo := data.diagInfo 355 if data.display.done { 356 // If we are done, show a summary of how many messages were printed. 357 if c := diagInfo.ErrorCount; c > 0 { 358 appendDiagMessage(fmt.Sprintf("%d %s%s%s", 359 c, colors.SpecError, english.PluralWord(c, "error", ""), colors.Reset)) 360 } 361 if c := diagInfo.WarningCount; c > 0 { 362 appendDiagMessage(fmt.Sprintf("%d %s%s%s", 363 c, colors.SpecWarning, english.PluralWord(c, "warning", ""), colors.Reset)) 364 } 365 if c := diagInfo.InfoCount; c > 0 { 366 appendDiagMessage(fmt.Sprintf("%d %s%s%s", 367 c, colors.SpecInfo, english.PluralWord(c, "message", ""), colors.Reset)) 368 } 369 if c := diagInfo.DebugCount; c > 0 { 370 appendDiagMessage(fmt.Sprintf("%d %s%s%s", 371 c, colors.SpecDebug, english.PluralWord(c, "debug", ""), colors.Reset)) 372 } 373 } else { 374 // If we're not totally done, and we're in the tree-view, just print out the last error (if 375 // there is one) next to the status message. This is helpful for long running tasks to know 376 // something bad has happened. However, once done, we print the diagnostics at the bottom, so we don't 377 // need to show this. 378 // 379 // if we're not in the tree-view (i.e. non-interactive mode), then we want to print out 380 // whatever the last diagnostics was that we got. This way, as we're hearing about 381 // diagnostic events, we're always printing out the last one. 382 383 diagnostic := data.diagInfo.LastDiag 384 if data.display.isTerminal && data.diagInfo.LastError != nil { 385 diagnostic = data.diagInfo.LastError 386 } 387 388 if diagnostic != nil { 389 eventMsg := data.display.renderProgressDiagEvent(*diagnostic, true /*includePrefix:*/) 390 if eventMsg != "" { 391 appendDiagMessage(eventMsg) 392 } 393 } 394 } 395 396 newLineIndex := strings.Index(diagMsg, "\n") 397 if newLineIndex >= 0 { 398 diagMsg = diagMsg[0:newLineIndex] 399 } 400 401 return diagMsg 402 } 403 404 func getDiffInfo(step engine.StepEventMetadata, action apitype.UpdateKind) string { 405 diffOutputs := action == apitype.RefreshUpdate 406 changesBuf := &bytes.Buffer{} 407 if step.Old != nil && step.New != nil { 408 var diff *resource.ObjectDiff 409 if step.DetailedDiff != nil { 410 diff = engine.TranslateDetailedDiff(&step) 411 } else if diffOutputs { 412 if step.Old.Outputs != nil && step.New.Outputs != nil { 413 diff = step.Old.Outputs.Diff(step.New.Outputs) 414 } 415 } else if step.Old.Inputs != nil && step.New.Inputs != nil { 416 diff = step.Old.Inputs.Diff(step.New.Inputs) 417 } 418 419 // Show a diff if either `provider` or `protect` changed; they might not show a diff via inputs or outputs, but 420 // it is still useful to show that these changed in output. 421 recordMetadataDiff := func(name string, old, new resource.PropertyValue) { 422 if old != new { 423 if diff == nil { 424 diff = &resource.ObjectDiff{ 425 Adds: make(resource.PropertyMap), 426 Deletes: make(resource.PropertyMap), 427 Sames: make(resource.PropertyMap), 428 Updates: make(map[resource.PropertyKey]resource.ValueDiff), 429 } 430 } 431 432 diff.Updates[resource.PropertyKey(name)] = resource.ValueDiff{Old: old, New: new} 433 } 434 } 435 436 recordMetadataDiff("provider", 437 resource.NewStringProperty(step.Old.Provider), resource.NewStringProperty(step.New.Provider)) 438 recordMetadataDiff("protect", 439 resource.NewBoolProperty(step.Old.Protect), resource.NewBoolProperty(step.New.Protect)) 440 441 if diff != nil { 442 writeString(changesBuf, "diff: ") 443 444 updates := make(resource.PropertyMap) 445 for k := range diff.Updates { 446 updates[k] = resource.PropertyValue{} 447 } 448 449 filteredKeys := func(m resource.PropertyMap) []string { 450 keys := make([]string, 0, len(m)) 451 for k := range m { 452 keys = append(keys, string(k)) 453 } 454 return keys 455 } 456 if include := step.Diffs; include != nil { 457 includeSet := make(map[resource.PropertyKey]bool) 458 for _, k := range include { 459 includeSet[k] = true 460 } 461 filteredKeys = func(m resource.PropertyMap) []string { 462 var filteredKeys []string 463 for k := range m { 464 if includeSet[k] { 465 filteredKeys = append(filteredKeys, string(k)) 466 } 467 } 468 return filteredKeys 469 } 470 } 471 472 writePropertyKeys(changesBuf, filteredKeys(diff.Adds), deploy.OpCreate) 473 writePropertyKeys(changesBuf, filteredKeys(diff.Deletes), deploy.OpDelete) 474 writePropertyKeys(changesBuf, filteredKeys(updates), deploy.OpUpdate) 475 } 476 } 477 478 fprintIgnoreError(changesBuf, colors.Reset) 479 return changesBuf.String() 480 } 481 482 func writePropertyKeys(b io.StringWriter, keys []string, op display.StepOp) { 483 if len(keys) > 0 { 484 writeString(b, strings.Trim(deploy.Prefix(op, true /*done*/), " ")) 485 486 sort.Strings(keys) 487 488 for index, k := range keys { 489 if index != 0 { 490 writeString(b, ",") 491 } 492 writeString(b, k) 493 } 494 495 writeString(b, colors.Reset) 496 } 497 }