github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/backend/display/diff.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 "math" 22 "os" 23 "sort" 24 "time" 25 26 "github.com/dustin/go-humanize/english" 27 28 "github.com/pulumi/pulumi/pkg/v3/engine" 29 "github.com/pulumi/pulumi/pkg/v3/resource/deploy" 30 "github.com/pulumi/pulumi/sdk/v3/go/common/diag" 31 "github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors" 32 "github.com/pulumi/pulumi/sdk/v3/go/common/resource" 33 "github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil" 34 "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" 35 ) 36 37 // ShowDiffEvents displays the engine events with the diff view. 38 func ShowDiffEvents(op string, events <-chan engine.Event, done chan<- bool, opts Options) { 39 40 prefix := fmt.Sprintf("%s%s...", cmdutil.EmojiOr("✨ ", "@ "), op) 41 42 stdout := opts.Stdout 43 if stdout == nil { 44 stdout = os.Stdout 45 } 46 stderr := opts.Stderr 47 if stderr == nil { 48 stderr = os.Stderr 49 } 50 51 var spinner cmdutil.Spinner 52 var ticker *time.Ticker 53 if stdout == os.Stdout && stderr == os.Stderr { 54 spinner, ticker = cmdutil.NewSpinnerAndTicker(prefix, nil, opts.Color, 8 /*timesPerSecond*/) 55 } else { 56 spinner = &nopSpinner{} 57 ticker = time.NewTicker(math.MaxInt64) 58 } 59 60 defer func() { 61 spinner.Reset() 62 ticker.Stop() 63 close(done) 64 }() 65 66 seen := make(map[resource.URN]engine.StepEventMetadata) 67 68 for { 69 select { 70 case <-ticker.C: 71 spinner.Tick() 72 case event := <-events: 73 spinner.Reset() 74 75 out := stdout 76 if event.Type == engine.DiagEvent { 77 payload := event.Payload().(engine.DiagEventPayload) 78 if payload.Severity == diag.Error || payload.Severity == diag.Warning { 79 out = stderr 80 } 81 } 82 83 msg := RenderDiffEvent(event, seen, opts) 84 if msg != "" && out != nil { 85 fprintIgnoreError(out, msg) 86 } 87 88 if event.Type == engine.CancelEvent { 89 return 90 } 91 } 92 } 93 } 94 95 func RenderDiffEvent(event engine.Event, seen map[resource.URN]engine.StepEventMetadata, opts Options) string { 96 97 switch event.Type { 98 case engine.CancelEvent: 99 return "" 100 101 // Currently, prelude, summary, and stdout events are printed the same for both the diff and 102 // progress displays. 103 case engine.PreludeEvent: 104 return renderPreludeEvent(event.Payload().(engine.PreludeEventPayload), opts) 105 case engine.SummaryEvent: 106 const wroteDiagnosticHeader = false 107 return renderSummaryEvent(event.Payload().(engine.SummaryEventPayload), wroteDiagnosticHeader, opts) 108 case engine.StdoutColorEvent: 109 return renderStdoutColorEvent(event.Payload().(engine.StdoutEventPayload), opts) 110 111 // Resource operations have very specific displays for either diff or progress displays. 112 // These functions should not be directly used by the progress display without validating 113 // that the display is appropriate for both. 114 case engine.ResourceOperationFailed: 115 return renderDiffResourceOperationFailedEvent(event.Payload().(engine.ResourceOperationFailedPayload), opts) 116 case engine.ResourceOutputsEvent: 117 return renderDiffResourceOutputsEvent(event.Payload().(engine.ResourceOutputsEventPayload), seen, opts) 118 case engine.ResourcePreEvent: 119 return renderDiffResourcePreEvent(event.Payload().(engine.ResourcePreEventPayload), seen, opts) 120 case engine.DiagEvent: 121 return renderDiffDiagEvent(event.Payload().(engine.DiagEventPayload), opts) 122 case engine.PolicyViolationEvent: 123 return renderDiffPolicyViolationEvent(event.Payload().(engine.PolicyViolationEventPayload), opts) 124 125 default: 126 contract.Failf("unknown event type '%s'", event.Type) 127 return "" 128 } 129 } 130 131 func renderDiffDiagEvent(payload engine.DiagEventPayload, opts Options) string { 132 if payload.Severity == diag.Debug && !opts.Debug { 133 return "" 134 } 135 return opts.Color.Colorize(payload.Prefix + payload.Message) 136 } 137 138 func renderDiffPolicyViolationEvent(payload engine.PolicyViolationEventPayload, opts Options) string { 139 return opts.Color.Colorize(payload.Prefix + payload.Message) 140 } 141 142 func renderStdoutColorEvent(payload engine.StdoutEventPayload, opts Options) string { 143 return opts.Color.Colorize(payload.Message) 144 } 145 146 func renderSummaryEvent(event engine.SummaryEventPayload, wroteDiagnosticHeader bool, opts Options) string { 147 148 changes := event.ResourceChanges 149 150 out := &bytes.Buffer{} 151 152 // If this is a failed preview, we only render the Policy Packs that ran. This is because rendering the summary 153 // for a failed preview may be surprising/misleading, as it does not describe the totality of the proposed changes 154 // (as the preview may have aborted when the error occurred). 155 if event.IsPreview && wroteDiagnosticHeader { 156 renderPolicyPacks(out, event.PolicyPacks, opts) 157 return out.String() 158 } 159 fprintIgnoreError(out, opts.Color.Colorize( 160 fmt.Sprintf("%sResources:%s\n", colors.SpecHeadline, colors.Reset))) 161 162 var planTo string 163 if event.IsPreview { 164 planTo = "to " 165 } 166 167 var changeKindCount = 0 168 var changeCount = 0 169 var sameCount = changes[deploy.OpSame] 170 171 // Now summarize all of the changes; we print sames a little differently. 172 for _, op := range deploy.StepOps { 173 // Ignore anything that didn't change, or is related to 'reads'. 'reads' are just an 174 // indication of the operations we were performing, and are not indicative of any sort of 175 // change to the system. 176 if op != deploy.OpSame && 177 op != deploy.OpRead && 178 op != deploy.OpReadDiscard && 179 op != deploy.OpReadReplacement { 180 181 if c := changes[op]; c > 0 { 182 opDescription := string(op) 183 if !event.IsPreview { 184 opDescription = deploy.PastTense(op) 185 } 186 187 // Increment the change count by the number of changes associated with this step kind 188 changeCount += c 189 190 // Increment the number of kinds of changes by one 191 changeKindCount++ 192 193 // Print a summary of the changes of this kind 194 fprintIgnoreError(out, opts.Color.Colorize( 195 fmt.Sprintf(" %s%d %s%s%s\n", deploy.Prefix(op, true /*done*/), c, planTo, opDescription, colors.Reset))) 196 } 197 } 198 } 199 200 summaryPieces := []string{} 201 if changeKindCount >= 2 { 202 // Only if we made multiple types of changes do we need to print out the total number of 203 // changes. i.e. we don't need "10 changes" and "+ 10 to create". We can just say "+ 10 to create" 204 summaryPieces = append(summaryPieces, fmt.Sprintf("%s%d %s%s", 205 colors.Bold, changeCount, english.PluralWord(changeCount, "change", ""), colors.Reset)) 206 } 207 208 if sameCount != 0 { 209 summaryPieces = append(summaryPieces, fmt.Sprintf("%d unchanged", sameCount)) 210 } 211 212 if len(summaryPieces) > 0 { 213 fprintfIgnoreError(out, " ") 214 215 for i, piece := range summaryPieces { 216 if i > 0 { 217 fprintfIgnoreError(out, ". ") 218 } 219 220 out.WriteString(opts.Color.Colorize(piece)) 221 } 222 223 fprintfIgnoreError(out, "\n") 224 } 225 226 // Print policy packs loaded. Data is rendered as a table of {policy-pack-name, version}. 227 renderPolicyPacks(out, event.PolicyPacks, opts) 228 229 // For actual deploys, we print some additional summary information 230 if !event.IsPreview { 231 // Round up to the nearest second. It's not useful to spit out time with 9 digits of 232 // precision. 233 roundedSeconds := int64(math.Ceil(event.Duration.Seconds())) 234 roundedDuration := time.Duration(roundedSeconds) * time.Second 235 236 fprintIgnoreError(out, opts.Color.Colorize(fmt.Sprintf("\n%sDuration:%s %s\n", 237 colors.SpecHeadline, colors.Reset, roundedDuration))) 238 } 239 240 return out.String() 241 } 242 243 func renderPolicyPacks(out io.Writer, policyPacks map[string]string, opts Options) { 244 if len(policyPacks) == 0 { 245 return 246 } 247 fprintIgnoreError(out, opts.Color.Colorize(fmt.Sprintf("\n%sPolicy Packs run:%s\n", 248 colors.SpecHeadline, colors.Reset))) 249 250 // Calculate column width for the `name` column 251 const nameColHeader = "Name" 252 maxNameLen := len(nameColHeader) 253 for pp := range policyPacks { 254 if l := len(pp); l > maxNameLen { 255 maxNameLen = l 256 } 257 } 258 259 // Print the column headers and the policy packs. 260 fprintIgnoreError(out, opts.Color.Colorize( 261 fmt.Sprintf(" %s%s%s\n", 262 columnHeader(nameColHeader), messagePadding(nameColHeader, maxNameLen, 2), 263 columnHeader("Version")))) 264 for pp, ver := range policyPacks { 265 fprintIgnoreError(out, opts.Color.Colorize( 266 fmt.Sprintf(" %s%s%s\n", pp, messagePadding(pp, maxNameLen, 2), ver))) 267 } 268 } 269 270 func renderPreludeEvent(event engine.PreludeEventPayload, opts Options) string { 271 // Only if we have been instructed to show configuration values will we print anything during the prelude. 272 if !opts.ShowConfig { 273 return "" 274 } 275 276 out := &bytes.Buffer{} 277 fprintIgnoreError(out, opts.Color.Colorize( 278 fmt.Sprintf("%sConfiguration:%s\n", colors.SpecUnimportant, colors.Reset))) 279 280 var keys []string 281 for key := range event.Config { 282 keys = append(keys, key) 283 } 284 sort.Strings(keys) 285 for _, key := range keys { 286 fprintfIgnoreError(out, " %v: %v\n", key, event.Config[key]) 287 } 288 289 return out.String() 290 } 291 292 func renderDiffResourceOperationFailedEvent( 293 payload engine.ResourceOperationFailedPayload, opts Options) string { 294 295 // It's not actually useful or interesting to print out any details about 296 // the resource state here, because we always assume that the resource state 297 // is unknown if an error occurs. 298 // 299 // In the future, once we get more fine-grained error messages from providers, 300 // we can provide useful diagnostics here. 301 302 return "" 303 } 304 305 func renderDiff( 306 out io.Writer, 307 metadata engine.StepEventMetadata, 308 planning, debug bool, 309 seen map[resource.URN]engine.StepEventMetadata, 310 opts Options) { 311 312 indent := getIndent(metadata, seen) 313 summary := getResourcePropertiesSummary(metadata, indent) 314 315 var details string 316 if metadata.DetailedDiff != nil { 317 var buf bytes.Buffer 318 if diff := engine.TranslateDetailedDiff(&metadata); diff != nil { 319 PrintObjectDiff(&buf, *diff, nil /*include*/, planning, indent+1, opts.SummaryDiff, opts.TruncateOutput, debug) 320 } else { 321 PrintObject( 322 &buf, metadata.Old.Inputs, planning, indent+1, deploy.OpSame, true /*prefix*/, opts.TruncateOutput, debug) 323 } 324 details = buf.String() 325 } else { 326 details = getResourcePropertiesDetails( 327 metadata, indent, planning, opts.SummaryDiff, opts.TruncateOutput, debug) 328 } 329 330 fprintIgnoreError(out, opts.Color.Colorize(summary)) 331 fprintIgnoreError(out, opts.Color.Colorize(details)) 332 fprintIgnoreError(out, opts.Color.Colorize(colors.Reset)) 333 } 334 335 func renderDiffResourcePreEvent( 336 payload engine.ResourcePreEventPayload, 337 seen map[resource.URN]engine.StepEventMetadata, 338 opts Options) string { 339 340 seen[payload.Metadata.URN] = payload.Metadata 341 if payload.Metadata.Op == deploy.OpRefresh || payload.Metadata.Op == deploy.OpImport { 342 return "" 343 } 344 345 out := &bytes.Buffer{} 346 if shouldShow(payload.Metadata, opts) || isRootStack(payload.Metadata) { 347 renderDiff(out, payload.Metadata, payload.Planning, payload.Debug, seen, opts) 348 } 349 return out.String() 350 } 351 352 func renderDiffResourceOutputsEvent( 353 payload engine.ResourceOutputsEventPayload, 354 seen map[resource.URN]engine.StepEventMetadata, 355 opts Options) string { 356 357 out := &bytes.Buffer{} 358 if shouldShow(payload.Metadata, opts) || isRootStack(payload.Metadata) { 359 // If this is the output step for an import, we actually want to display the diff at this point. 360 if payload.Metadata.Op == deploy.OpImport { 361 renderDiff(out, payload.Metadata, payload.Planning, payload.Debug, seen, opts) 362 return out.String() 363 } 364 365 indent := getIndent(payload.Metadata, seen) 366 367 refresh := false // are these outputs from a refresh? 368 if m, has := seen[payload.Metadata.URN]; has && m.Op == deploy.OpRefresh { 369 refresh = true 370 summary := getResourcePropertiesSummary(payload.Metadata, indent) 371 fprintIgnoreError(out, opts.Color.Colorize(summary)) 372 } 373 374 if !opts.SuppressOutputs { 375 // We want to hide same outputs if we're doing a read and the user didn't ask to see 376 // things that are the same. 377 text := getResourceOutputsPropertiesString( 378 payload.Metadata, indent+1, payload.Planning, 379 payload.Debug, refresh, opts.ShowSameResources) 380 if text != "" { 381 header := fmt.Sprintf("%v%v--outputs:--%v\n", 382 deploy.Color(payload.Metadata.Op), getIndentationString(indent+1, payload.Metadata.Op, false), colors.Reset) 383 fprintfIgnoreError(out, opts.Color.Colorize(header)) 384 fprintIgnoreError(out, opts.Color.Colorize(text)) 385 } 386 } 387 } 388 return out.String() 389 }