pulumi/pkg/backend/display/rows.go

529 lines
15 KiB
Go

// 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.
package display
import (
"bytes"
"fmt"
"io"
"sort"
"strings"
"github.com/dustin/go-humanize/english"
"github.com/pulumi/pulumi/pkg/v3/display"
"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/slice"
)
type Row interface {
DisplayOrderIndex() int
SetDisplayOrderIndex(index int)
ColorizedColumns() []string
ColorizedSuffix() string
HideRowIfUnnecessary() bool
SetHideRowIfUnnecessary(value bool)
}
type ResourceRow interface {
Row
Step() engine.StepEventMetadata
SetStep(step engine.StepEventMetadata)
AddOutputStep(step engine.StepEventMetadata)
// The tick we were on when we created this row. Purely used for generating an
// ellipses to show progress for in-flight resources.
Tick() int
IsDone() bool
SetFailed()
DiagInfo() *DiagInfo
PolicyPayloads() []engine.PolicyViolationEventPayload
PolicyRemediationPayloads() []engine.PolicyRemediationEventPayload
RecordDiagEvent(diagEvent engine.Event)
RecordPolicyViolationEvent(diagEvent engine.Event)
RecordPolicyRemediationEvent(diagEvent engine.Event)
}
// Implementation of a Row, used for the header of the grid.
type headerRowData struct {
display *ProgressDisplay
columns []string
}
func (data *headerRowData) HideRowIfUnnecessary() bool {
return false
}
func (data *headerRowData) SetHideRowIfUnnecessary(value bool) {
}
func (data *headerRowData) DisplayOrderIndex() int {
// sort the header before all other rows
return -1
}
func (data *headerRowData) SetDisplayOrderIndex(time int) {
// Nothing to do here. Header is always at the same index.
}
func (data *headerRowData) ColorizedColumns() []string {
if len(data.columns) == 0 {
header := func(msg string) string {
return columnHeader(msg)
}
var statusColumn string
if data.display.isPreview {
statusColumn = header("Plan")
} else {
statusColumn = header("Status")
}
data.columns = []string{"", header("Type"), header("Name"), statusColumn, header("Info")}
}
return data.columns
}
func (data *headerRowData) ColorizedSuffix() string {
return ""
}
// resourceRowData is the implementation of a row used for all the resource rows in the grid.
type resourceRowData struct {
displayOrderIndex int
display *ProgressDisplay
// The change that the engine wants apply to that resource.
step engine.StepEventMetadata
outputSteps []engine.StepEventMetadata
// The tick we were on when we created this row. Purely used for generating an
// ellipses to show progress for in-flight resources.
tick int
// If we failed this operation for any reason.
failed bool
diagInfo *DiagInfo
policyPayloads []engine.PolicyViolationEventPayload
policyRemediationPayloads []engine.PolicyRemediationEventPayload
// If this row should be hidden by default. We will hide unless we have any child nodes
// we need to show.
hideRowIfUnnecessary bool
}
func (data *resourceRowData) DisplayOrderIndex() int {
// sort the header before all other rows
return data.displayOrderIndex
}
func (data *resourceRowData) SetDisplayOrderIndex(index int) {
// only set this if it's the first time.
if data.displayOrderIndex == 0 {
data.displayOrderIndex = index
}
}
func (data *resourceRowData) HideRowIfUnnecessary() bool {
return data.hideRowIfUnnecessary
}
func (data *resourceRowData) SetHideRowIfUnnecessary(value bool) {
data.hideRowIfUnnecessary = value
}
func (data *resourceRowData) Step() engine.StepEventMetadata {
return data.step
}
func (data *resourceRowData) SetStep(step engine.StepEventMetadata) {
data.step = step
}
func (data *resourceRowData) AddOutputStep(step engine.StepEventMetadata) {
data.outputSteps = append(data.outputSteps, step)
}
func (data *resourceRowData) Tick() int {
return data.tick
}
func (data *resourceRowData) Failed() bool {
return data.failed
}
func (data *resourceRowData) SetFailed() {
data.failed = true
}
func (data *resourceRowData) DiagInfo() *DiagInfo {
return data.diagInfo
}
func (data *resourceRowData) RecordDiagEvent(event engine.Event) {
payload := event.Payload().(engine.DiagEventPayload)
data.recordDiagEventPayload(payload)
}
func (data *resourceRowData) recordDiagEventPayload(payload engine.DiagEventPayload) {
diagInfo := data.diagInfo
diagInfo.LastDiag = &payload
if payload.Severity == diag.Error {
diagInfo.LastError = &payload
}
if diagInfo.StreamIDToDiagPayloads == nil {
diagInfo.StreamIDToDiagPayloads = make(map[int32][]engine.DiagEventPayload)
}
payloads := diagInfo.StreamIDToDiagPayloads[payload.StreamID]
payloads = append(payloads, payload)
diagInfo.StreamIDToDiagPayloads[payload.StreamID] = payloads
if !payload.Ephemeral {
switch payload.Severity {
case diag.Error:
diagInfo.ErrorCount++
case diag.Warning:
diagInfo.WarningCount++
case diag.Infoerr:
diagInfo.InfoCount++
case diag.Info:
diagInfo.InfoCount++
case diag.Debug:
diagInfo.DebugCount++
}
}
}
// PolicyPayloads returns the PolicyViolationEventPayload object associated with the resourceRowData.
func (data *resourceRowData) PolicyPayloads() []engine.PolicyViolationEventPayload {
return data.policyPayloads
}
// RecordPolicyViolationEvent records a policy event with the resourceRowData.
func (data *resourceRowData) RecordPolicyViolationEvent(event engine.Event) {
pePayload := event.Payload().(engine.PolicyViolationEventPayload)
data.policyPayloads = append(data.policyPayloads, pePayload)
}
// PolicyRemediationPayloads returns all policy remediation event payloads that have been registered.
func (data *resourceRowData) PolicyRemediationPayloads() []engine.PolicyRemediationEventPayload {
return data.policyRemediationPayloads
}
// RecordPolicyRemediationEvent records a policy remediation with the resourceRowData.
func (data *resourceRowData) RecordPolicyRemediationEvent(event engine.Event) {
tPayload := event.Payload().(engine.PolicyRemediationEventPayload)
data.policyRemediationPayloads = append(data.policyRemediationPayloads, tPayload)
}
type column int
const (
opColumn column = 0
typeColumn column = 1
nameColumn column = 2
statusColumn column = 3
infoColumn column = 4
)
func (data *resourceRowData) IsDone() bool {
if data.failed {
// consider a failed resource 'done'.
return true
}
if data.display.done {
// if the display is done, then we're definitely done.
return true
}
if isRootStack(data.step) {
// the root stack only becomes 'done' once the program has completed (i.e. the condition
// checked just above this). If the program is not finished, then always show the root
// stack as not done so the user sees "running..." presented for it.
return false
}
// We're done if we have the output-step for whatever step operation we're performing
return data.ContainsOutputsStep(data.step.Op)
}
func (data *resourceRowData) ContainsOutputsStep(op display.StepOp) bool {
for _, s := range data.outputSteps {
if s.Op == op {
return true
}
}
return false
}
func (data *resourceRowData) ColorizedSuffix() string {
if !data.IsDone() && data.display.isTerminal {
op := data.display.getStepOp(data.step)
if op != deploy.OpSame || isRootURN(data.step.URN) {
suffixes := data.display.suffixesArray
ellipses := suffixes[(data.tick+data.display.currentTick)%len(suffixes)]
return deploy.ColorProgress(op) + ellipses + colors.Reset
}
}
return ""
}
func (data *resourceRowData) ColorizedColumns() []string {
step := data.step
urn := data.step.URN
if urn == "" {
// If we don't have a URN yet, mock parent it to the global stack.
urn = resource.DefaultRootStackURN(data.display.stack.Q(), data.display.proj)
}
name := urn.Name()
typ := urn.Type().DisplayName()
done := data.IsDone()
columns := make([]string, 5)
columns[opColumn] = data.display.getStepOpLabel(step, done)
columns[typeColumn] = typ
columns[nameColumn] = name
diagInfo := data.diagInfo
failed := data.failed || diagInfo.ErrorCount > 0
columns[statusColumn] = data.display.getStepStatus(step, done, failed)
columns[infoColumn] = data.getInfoColumn()
return columns
}
// addRetainStatusFlag adds a "[retain]" suffix to the input string if the resource is marked as
// RetainOnDelete and the step will discard the resource.
func addRetainStatusFlag(status string, step engine.StepEventMetadata) string {
if step.Old == nil || !step.Old.RetainOnDelete {
return status
}
switch step.Op {
// Deletes and Replacements should indicate retain on delete behavior as they can leave
// untracked resources in the environment.
case deploy.OpDelete, deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced:
status += "[retain]"
}
return status
}
func (data *resourceRowData) getInfoColumn() string {
step := data.step
switch step.Op {
case deploy.OpCreateReplacement, deploy.OpDeleteReplaced:
// if we're doing a replacement, see if we can find a replace step that contains useful
// information to display.
for _, outputStep := range data.outputSteps {
if outputStep.Op == deploy.OpReplace {
step = outputStep
}
}
case deploy.OpImport, deploy.OpImportReplacement:
// If we're doing an import, see if we have the imported state to diff.
for _, outputStep := range data.outputSteps {
if outputStep.Op == step.Op {
step = outputStep
}
}
}
var diagMsg string
appendDiagMessage := func(msg string) {
if diagMsg != "" {
diagMsg += "; "
}
diagMsg += msg
}
changes := getDiffInfo(step, data.display.action)
if colors.Never.Colorize(changes) != "" {
appendDiagMessage("[" + changes + "]")
}
diagInfo := data.diagInfo
if data.display.done {
// If we are done, show a summary of how many messages were printed.
if c := diagInfo.ErrorCount; c > 0 {
appendDiagMessage(fmt.Sprintf("%d %s%s%s",
c, colors.SpecError, english.PluralWord(c, "error", ""), colors.Reset))
}
if c := diagInfo.WarningCount; c > 0 {
appendDiagMessage(fmt.Sprintf("%d %s%s%s",
c, colors.SpecWarning, english.PluralWord(c, "warning", ""), colors.Reset))
}
if c := diagInfo.InfoCount; c > 0 {
appendDiagMessage(fmt.Sprintf("%d %s%s%s",
c, colors.SpecInfo, english.PluralWord(c, "message", ""), colors.Reset))
}
if c := diagInfo.DebugCount; c > 0 {
appendDiagMessage(fmt.Sprintf("%d %s%s%s",
c, colors.SpecDebug, english.PluralWord(c, "debug", ""), colors.Reset))
}
} else {
// If we're not totally done, and we're in the tree-view, just print out the last error (if
// there is one) next to the status message. This is helpful for long running tasks to know
// something bad has happened. However, once done, we print the diagnostics at the bottom, so we don't
// need to show this.
//
// if we're not in the tree-view (i.e. non-interactive mode), then we want to print out
// whatever the last diagnostics was that we got. This way, as we're hearing about
// diagnostic events, we're always printing out the last one.
diagnostic := data.diagInfo.LastDiag
if data.display.isTerminal && data.diagInfo.LastError != nil {
diagnostic = data.diagInfo.LastError
}
if diagnostic != nil {
eventMsg := data.display.renderProgressDiagEvent(*diagnostic, true /*includePrefix:*/)
if eventMsg != "" {
appendDiagMessage(eventMsg)
}
}
}
newLineIndex := strings.Index(diagMsg, "\n")
if newLineIndex >= 0 {
diagMsg = diagMsg[0:newLineIndex]
}
return diagMsg
}
func getDiffInfo(step engine.StepEventMetadata, action apitype.UpdateKind) string {
changesBuf := &bytes.Buffer{}
if step.Old != nil && step.New != nil {
var diff *resource.ObjectDiff
// An OpSame might have a diff due to metadata changes (e.g. protect) but we should never print a property diff,
// even if the properties appear to have changed. See https://github.com/pulumi/pulumi/issues/15944 for context.
if step.Op != deploy.OpSame {
if step.DetailedDiff != nil {
diff = engine.TranslateDetailedDiff(&step, false)
} else if step.Old.Inputs != nil && step.New.Inputs != nil {
diff = step.Old.Inputs.Diff(step.New.Inputs)
}
}
// Show a diff if either `provider` or `protect` changed; they might not show a diff via inputs or outputs, but
// it is still useful to show that these changed in output.
recordMetadataDiff := func(name string, old, new resource.PropertyValue) {
if old != new {
if diff == nil {
diff = &resource.ObjectDiff{
Adds: make(resource.PropertyMap),
Deletes: make(resource.PropertyMap),
Sames: make(resource.PropertyMap),
Updates: make(map[resource.PropertyKey]resource.ValueDiff),
}
}
diff.Updates[resource.PropertyKey(name)] = resource.ValueDiff{Old: old, New: new}
}
}
recordMetadataDiff("provider",
resource.NewStringProperty(step.Old.Provider), resource.NewStringProperty(step.New.Provider))
recordMetadataDiff("protect",
resource.NewBoolProperty(step.Old.Protect), resource.NewBoolProperty(step.New.Protect))
writeShortDiff(changesBuf, diff, step.Diffs)
}
fprintIgnoreError(changesBuf, colors.Reset)
return changesBuf.String()
}
func writeShortDiff(changesBuf io.StringWriter, diff *resource.ObjectDiff, include []resource.PropertyKey) {
if diff != nil {
writeString(changesBuf, "diff: ")
updates := make(resource.PropertyMap)
for k := range diff.Updates {
updates[k] = resource.PropertyValue{}
}
filteredKeys := func(m resource.PropertyMap) []string {
keys := slice.Prealloc[string](len(m))
for k := range m {
keys = append(keys, string(k))
}
return keys
}
if len(include) > 0 {
includeSet := make(map[resource.PropertyKey]bool)
for _, k := range include {
includeSet[k] = true
}
filteredKeys = func(m resource.PropertyMap) []string {
var filteredKeys []string
for k := range m {
if includeSet[k] {
filteredKeys = append(filteredKeys, string(k))
}
}
return filteredKeys
}
}
writePropertyKeys(changesBuf, filteredKeys(diff.Adds), deploy.OpCreate)
writePropertyKeys(changesBuf, filteredKeys(diff.Deletes), deploy.OpDelete)
writePropertyKeys(changesBuf, filteredKeys(updates), deploy.OpUpdate)
}
}
func writePropertyKeys(b io.StringWriter, keys []string, op display.StepOp) {
if len(keys) > 0 {
writeString(b, strings.Trim(deploy.Prefix(op, true /*done*/), " "))
sort.Strings(keys)
for index, k := range keys {
if index != 0 {
writeString(b, ",")
}
writeString(b, k)
}
writeString(b, colors.Reset)
}
}