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  }