improve PAC error output (#3881)
* Improve pac error output * Print policy packs applied if no violations present * Remove unnecessary policyInfo struct * Adjust integration test
This commit is contained in:
parent
ba046b063b
commit
abd1b98003
|
@ -62,6 +62,7 @@ func makeActionProgress(id string, action string) Progress {
|
|||
return Progress{ID: id, Action: action}
|
||||
}
|
||||
|
||||
// DiagInfo contains the bundle of diagnostic information for a single resource.
|
||||
type DiagInfo struct {
|
||||
ErrorCount, WarningCount, InfoCount, DebugCount int
|
||||
|
||||
|
@ -80,10 +81,11 @@ type DiagInfo struct {
|
|||
// diagnostics for a resource.
|
||||
//
|
||||
// Diagnostic events are bucketed by their associated stream ID (with 0 being the default
|
||||
// stream)
|
||||
// stream).
|
||||
StreamIDToDiagPayloads map[int32][]engine.DiagEventPayload
|
||||
}
|
||||
|
||||
// ProgressDisplay organizes all the information needed for a dynamically updated "progress" view of an update.
|
||||
type ProgressDisplay struct {
|
||||
opts Options
|
||||
progressOutput chan<- Progress
|
||||
|
@ -168,6 +170,8 @@ var (
|
|||
// simple regex to take our names like "aws:function:Function" and convert to
|
||||
// "aws:Function"
|
||||
typeNameRegex = regexp.MustCompile("^(.*):(.*)/(.*):(.*)$")
|
||||
// policyPayloads is a collection of policy violation events for a single resource.
|
||||
policyPayloads []engine.PolicyViolationEventPayload
|
||||
)
|
||||
|
||||
func simplifyTypeName(typ tokens.Type) string {
|
||||
|
@ -649,7 +653,7 @@ func (display *ProgressDisplay) processEndSteps() {
|
|||
}
|
||||
}
|
||||
|
||||
// transition the display to the 'done' state. this will transitively cause all
|
||||
// Transition the display to the 'done' state. This will transitively cause all
|
||||
// rows to become done.
|
||||
display.done = true
|
||||
|
||||
|
@ -661,99 +665,195 @@ func (display *ProgressDisplay) processEndSteps() {
|
|||
}
|
||||
}
|
||||
|
||||
// Now refresh everything. this ensures that we go back and remove things like the diagnostic
|
||||
// Now refresh everything. This ensures that we go back and remove things like the diagnostic
|
||||
// messages from a status message (since we're going to print them all) below. Note, this will
|
||||
// only do something in a terminal. This i what we want, because if we're not in a terminal we
|
||||
// only do something in a terminal. This is what we want, because if we're not in a terminal we
|
||||
// don't really want to reprint any finished items we've already printed.
|
||||
display.refreshAllRowsIfInTerminal()
|
||||
|
||||
// Print all diagnostics we've seen.
|
||||
// Render several "sections" of output based on available data as applicable.
|
||||
display.writeBlankLine()
|
||||
wroteDiagnosticHeader := display.printDiagnostics()
|
||||
wrotePolicyViolations := display.printPolicyViolations()
|
||||
display.printOutputs()
|
||||
// If no policies violated, print policy packs applied.
|
||||
if !wrotePolicyViolations {
|
||||
display.printSummary(wroteDiagnosticHeader)
|
||||
}
|
||||
}
|
||||
|
||||
// printDiagnostics prints a new "Diagnostics:" section with all of the diagnostics grouped by
|
||||
// resource. If no diagnostics were emitted, prints nothing.
|
||||
func (display *ProgressDisplay) printDiagnostics() bool {
|
||||
// Since we display diagnostic information eagerly, we need to keep track of the first
|
||||
// time we wrote some output so we don't inadvertently print the header twice.
|
||||
wroteDiagnosticHeader := false
|
||||
|
||||
for _, row := range display.eventUrnToResourceRow {
|
||||
// The header for the diagnogistics grouped by resource, e.g. "aws:apigateway:RestApi (accountsApi):"
|
||||
wroteResourceHeader := false
|
||||
|
||||
// Each row in the display corresponded with a resource, and that resource could have emitted
|
||||
// diagnostics to various streams.
|
||||
for id, payloads := range row.DiagInfo().StreamIDToDiagPayloads {
|
||||
if len(payloads) > 0 {
|
||||
if id != 0 {
|
||||
// for the non-default stream merge all the messages from the stream into a single
|
||||
// message.
|
||||
p := display.mergeStreamPayloadsToSinglePayload(payloads)
|
||||
payloads = []engine.DiagEventPayload{p}
|
||||
}
|
||||
|
||||
wrote := false
|
||||
for _, v := range payloads {
|
||||
if v.Ephemeral {
|
||||
continue
|
||||
}
|
||||
|
||||
msg := display.renderProgressDiagEvent(v, true /*includePrefix:*/)
|
||||
|
||||
lines := splitIntoDisplayableLines(msg)
|
||||
if len(lines) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if !wroteDiagnosticHeader {
|
||||
wroteDiagnosticHeader = true
|
||||
display.writeBlankLine()
|
||||
display.writeSimpleMessage(
|
||||
display.opts.Color.Colorize(colors.SpecHeadline + "Diagnostics:" + colors.Reset))
|
||||
}
|
||||
|
||||
if !wroteResourceHeader {
|
||||
wroteResourceHeader = true
|
||||
columns := row.ColorizedColumns()
|
||||
display.writeSimpleMessage(" " +
|
||||
display.opts.Color.Colorize(
|
||||
colors.BrightBlue+columns[typeColumn]+" ("+columns[nameColumn]+"):"+colors.Reset))
|
||||
}
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimRightFunc(line, unicode.IsSpace)
|
||||
display.writeSimpleMessage(" " + line)
|
||||
}
|
||||
|
||||
wrote = true
|
||||
}
|
||||
|
||||
if wrote {
|
||||
display.writeBlankLine()
|
||||
}
|
||||
if len(payloads) == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we get stack outputs, display them at the end.
|
||||
var wroteOutputs bool
|
||||
if display.stackUrn != "" && !display.opts.SuppressOutputs {
|
||||
stackStep := display.eventUrnToResourceRow[display.stackUrn].Step()
|
||||
if id != 0 {
|
||||
// For the non-default stream merge all the messages from the stream into a single
|
||||
// message.
|
||||
p := display.mergeStreamPayloadsToSinglePayload(payloads)
|
||||
payloads = []engine.DiagEventPayload{p}
|
||||
}
|
||||
|
||||
props := engine.GetResourceOutputsPropertiesString(
|
||||
stackStep, 1, display.isPreview, display.opts.Debug,
|
||||
false /* refresh */, display.opts.ShowSameResources)
|
||||
if props != "" {
|
||||
if !wroteDiagnosticHeader {
|
||||
// Did we write any diagnostic information for the resource x stream?
|
||||
wrote := false
|
||||
for _, v := range payloads {
|
||||
if v.Ephemeral {
|
||||
continue
|
||||
}
|
||||
|
||||
msg := display.renderProgressDiagEvent(v, true /*includePrefix:*/)
|
||||
|
||||
lines := splitIntoDisplayableLines(msg)
|
||||
if len(lines) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// If we haven't printed the Diagnostics header, do so now.
|
||||
if !wroteDiagnosticHeader {
|
||||
wroteDiagnosticHeader = true
|
||||
display.writeSimpleMessage(
|
||||
display.opts.Color.Colorize(colors.SpecHeadline + "Diagnostics:" + colors.Reset))
|
||||
}
|
||||
// If we haven't printed the header for the resource, do so now.
|
||||
if !wroteResourceHeader {
|
||||
wroteResourceHeader = true
|
||||
columns := row.ColorizedColumns()
|
||||
display.writeSimpleMessage(" " +
|
||||
display.opts.Color.Colorize(
|
||||
colors.BrightBlue+columns[typeColumn]+" ("+columns[nameColumn]+"):"+colors.Reset))
|
||||
}
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimRightFunc(line, unicode.IsSpace)
|
||||
display.writeSimpleMessage(" " + line)
|
||||
}
|
||||
|
||||
wrote = true
|
||||
}
|
||||
|
||||
if wrote {
|
||||
display.writeBlankLine()
|
||||
}
|
||||
|
||||
wroteOutputs = true
|
||||
display.writeSimpleMessage(colors.SpecHeadline + "Outputs:" + colors.Reset)
|
||||
display.writeSimpleMessage(props)
|
||||
}
|
||||
}
|
||||
|
||||
// print the summary
|
||||
if display.summaryEventPayload != nil {
|
||||
if !wroteDiagnosticHeader && !wroteOutputs {
|
||||
display.writeBlankLine()
|
||||
}
|
||||
|
||||
msg := renderSummaryEvent(display.action, *display.summaryEventPayload, wroteDiagnosticHeader, display.opts)
|
||||
display.writeSimpleMessage(msg)
|
||||
}
|
||||
return wroteDiagnosticHeader
|
||||
}
|
||||
|
||||
// printPolicyViolations prints a new "Policy Violation:" section with all of the violations
|
||||
// grouped by policy pack. If no policy violations were encountered, prints nothing.
|
||||
func (display *ProgressDisplay) printPolicyViolations() bool {
|
||||
// Loop through every resource and gather up all policy violations encountered.
|
||||
var policyEvents []engine.PolicyViolationEventPayload
|
||||
for _, row := range display.eventUrnToResourceRow {
|
||||
policyPayloads := row.PolicyPayloads()
|
||||
if len(policyPayloads) == 0 {
|
||||
continue
|
||||
}
|
||||
policyEvents = append(policyEvents, policyPayloads...)
|
||||
}
|
||||
if len(policyEvents) == 0 {
|
||||
return false
|
||||
}
|
||||
// Sort policy events by: policy pack name, policy pack version, enforcement level,
|
||||
// policy name, and finally the URN of the resource.
|
||||
sort.SliceStable(policyEvents, func(i, j int) bool {
|
||||
eventI, eventJ := policyEvents[i], policyEvents[j]
|
||||
if packNameCmp := strings.Compare(
|
||||
eventI.PolicyPackName,
|
||||
eventJ.PolicyPackName); packNameCmp != 0 {
|
||||
return packNameCmp < 0
|
||||
}
|
||||
if packVerCmp := strings.Compare(
|
||||
eventI.PolicyPackVersion,
|
||||
eventJ.PolicyPackVersion); packVerCmp != 0 {
|
||||
return packVerCmp < 0
|
||||
}
|
||||
if enfLevelCmp := strings.Compare(
|
||||
string(eventI.EnforcementLevel),
|
||||
string(eventJ.EnforcementLevel)); enfLevelCmp != 0 {
|
||||
return enfLevelCmp < 0
|
||||
}
|
||||
if policyNameCmp := strings.Compare(
|
||||
eventI.PolicyName,
|
||||
eventJ.PolicyName); policyNameCmp != 0 {
|
||||
return policyNameCmp < 0
|
||||
}
|
||||
urnCmp := strings.Compare(
|
||||
string(eventI.ResourceURN),
|
||||
string(eventJ.ResourceURN))
|
||||
return urnCmp < 0
|
||||
})
|
||||
|
||||
// Print every policy violation, printing a new header when necessary.
|
||||
display.writeSimpleMessage(display.opts.Color.Colorize(colors.SpecHeadline + "Policy Violations:" + colors.Reset))
|
||||
|
||||
for _, policyEvent := range policyEvents {
|
||||
// Print the individual policy event.
|
||||
c := colors.SpecImportant
|
||||
if policyEvent.EnforcementLevel == apitype.Mandatory {
|
||||
c = colors.SpecError
|
||||
}
|
||||
|
||||
policyNameLine := fmt.Sprintf(" %s[%s] %s v%s %s %s (%s)",
|
||||
c, policyEvent.EnforcementLevel,
|
||||
policyEvent.PolicyPackName,
|
||||
policyEvent.PolicyPackVersion, colors.Reset,
|
||||
policyEvent.PolicyName,
|
||||
policyEvent.ResourceURN.Name())
|
||||
display.writeSimpleMessage(policyNameLine)
|
||||
|
||||
// The message may span multiple lines, so we massage it so it will be indented properly.
|
||||
message := strings.ReplaceAll(policyEvent.Message, "\n", "\n ")
|
||||
messageLine := fmt.Sprintf(" %s", message)
|
||||
display.writeSimpleMessage(messageLine)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// printOutputs prints the Stack's outputs for the display in a new section, if appropriate.
|
||||
func (display *ProgressDisplay) printOutputs() {
|
||||
// Printing the stack's outputs wasn't desired.
|
||||
if display.opts.SuppressOutputs {
|
||||
return
|
||||
}
|
||||
// Cannot display outputs for the stack if we don't know its URN.
|
||||
if display.stackUrn == "" {
|
||||
return
|
||||
}
|
||||
|
||||
stackStep := display.eventUrnToResourceRow[display.stackUrn].Step()
|
||||
|
||||
props := engine.GetResourceOutputsPropertiesString(
|
||||
stackStep, 1, display.isPreview, display.opts.Debug,
|
||||
false /* refresh */, display.opts.ShowSameResources)
|
||||
if props != "" {
|
||||
display.writeSimpleMessage(colors.SpecHeadline + "Outputs:" + colors.Reset)
|
||||
display.writeSimpleMessage(props)
|
||||
}
|
||||
}
|
||||
|
||||
// printSummary prints the Stack's SummaryEvent in a new section if applicable.
|
||||
func (display *ProgressDisplay) printSummary(wroteDiagnosticHeader bool) {
|
||||
// If we never saw the SummaryEvent payload, we have nothing to do.
|
||||
if display.summaryEventPayload == nil {
|
||||
return
|
||||
}
|
||||
|
||||
msg := renderSummaryEvent(display.action, *display.summaryEventPayload, wroteDiagnosticHeader, display.opts)
|
||||
display.writeSimpleMessage(msg)
|
||||
}
|
||||
|
||||
func (display *ProgressDisplay) mergeStreamPayloadsToSinglePayload(
|
||||
|
@ -838,6 +938,7 @@ func (display *ProgressDisplay) getRowForURN(urn resource.URN, metadata *engine.
|
|||
display: display,
|
||||
tick: display.currentTick,
|
||||
diagInfo: &DiagInfo{},
|
||||
policyPayloads: policyPayloads,
|
||||
step: step,
|
||||
hideRowIfUnnecessary: true,
|
||||
}
|
||||
|
@ -872,7 +973,7 @@ func (display *ProgressDisplay) processNormalEvent(event engine.Event) {
|
|||
}
|
||||
return
|
||||
case engine.SummaryEvent:
|
||||
// keep track of the summar event so that we can display it after all other
|
||||
// keep track of the summary event so that we can display it after all other
|
||||
// resource-related events we receive.
|
||||
payload := event.Payload.(engine.SummaryEventPayload)
|
||||
display.summaryEventPayload = &payload
|
||||
|
@ -1016,6 +1117,7 @@ func (display *ProgressDisplay) ensureHeaderAndStackRows() {
|
|||
display: display,
|
||||
tick: display.currentTick,
|
||||
diagInfo: &DiagInfo{},
|
||||
policyPayloads: policyPayloads,
|
||||
step: engine.StepEventMetadata{Op: deploy.OpSame},
|
||||
hideRowIfUnnecessary: false,
|
||||
}
|
||||
|
|
|
@ -22,13 +22,11 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/dustin/go-humanize/english"
|
||||
"github.com/pulumi/pulumi/pkg/apitype"
|
||||
"github.com/pulumi/pulumi/pkg/diag"
|
||||
"github.com/pulumi/pulumi/pkg/diag/colors"
|
||||
"github.com/pulumi/pulumi/pkg/engine"
|
||||
"github.com/pulumi/pulumi/pkg/resource"
|
||||
"github.com/pulumi/pulumi/pkg/resource/deploy"
|
||||
"github.com/pulumi/pulumi/pkg/util/contract"
|
||||
)
|
||||
|
||||
type Row interface {
|
||||
|
@ -58,6 +56,8 @@ type ResourceRow interface {
|
|||
SetFailed()
|
||||
|
||||
DiagInfo() *DiagInfo
|
||||
PolicyPayloads() []engine.PolicyViolationEventPayload
|
||||
|
||||
RecordDiagEvent(diagEvent engine.Event)
|
||||
RecordPolicyViolationEvent(diagEvent engine.Event)
|
||||
}
|
||||
|
@ -106,7 +106,7 @@ func (data *headerRowData) ColorizedSuffix() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// Implementation of a row used for all the resource rows in the grid.
|
||||
// resourceRowData is the implementation of a row used for all the resource rows in the grid.
|
||||
type resourceRowData struct {
|
||||
displayOrderIndex int
|
||||
|
||||
|
@ -123,7 +123,8 @@ type resourceRowData struct {
|
|||
// If we failed this operation for any reason.
|
||||
failed bool
|
||||
|
||||
diagInfo *DiagInfo
|
||||
diagInfo *DiagInfo
|
||||
policyPayloads []engine.PolicyViolationEventPayload
|
||||
|
||||
// If this row should be hidden by default. We will hide unless we have any child nodes
|
||||
// we need to show.
|
||||
|
@ -215,31 +216,15 @@ func (data *resourceRowData) recordDiagEventPayload(payload engine.DiagEventPayl
|
|||
}
|
||||
}
|
||||
|
||||
// PolicyInfo returns the PolicyInfo 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) {
|
||||
|
||||
//
|
||||
// NOTE: The display code only understands DiagEvents. We convert the policy violation events
|
||||
// accordingly so they can be displayed.
|
||||
//
|
||||
|
||||
pePayload := event.Payload.(engine.PolicyViolationEventPayload)
|
||||
|
||||
payload := engine.DiagEventPayload{
|
||||
URN: pePayload.ResourceURN,
|
||||
Prefix: pePayload.Prefix,
|
||||
Message: pePayload.Message,
|
||||
Color: pePayload.Color}
|
||||
|
||||
switch pePayload.EnforcementLevel {
|
||||
case apitype.Mandatory:
|
||||
payload.Severity = diag.Error
|
||||
case apitype.Advisory:
|
||||
payload.Severity = diag.Warning
|
||||
default:
|
||||
contract.Failf("Unknown enforcement level %q", pePayload.EnforcementLevel)
|
||||
}
|
||||
|
||||
data.recordDiagEventPayload(payload)
|
||||
data.policyPayloads = append(data.policyPayloads, pePayload)
|
||||
}
|
||||
|
||||
type column int
|
||||
|
|
|
@ -181,7 +181,7 @@ func TestProjectMain(t *testing.T) {
|
|||
e.RunCommand("pulumi", "login", "--cloud-url", e.LocalURL())
|
||||
e.RunCommand("pulumi", "stack", "init", "main-abs")
|
||||
stdout, stderr := e.RunCommandExpectError("pulumi", "up", "--non-interactive", "--skip-preview")
|
||||
assert.Equal(t, "Updating (main-abs):\n", stdout)
|
||||
assert.Equal(t, "Updating (main-abs):\n \n", stdout)
|
||||
assert.Contains(t, stderr, "project 'main' must be a relative path")
|
||||
e.RunCommand("pulumi", "stack", "rm", "--yes")
|
||||
})
|
||||
|
@ -197,7 +197,7 @@ func TestProjectMain(t *testing.T) {
|
|||
e.RunCommand("pulumi", "login", "--cloud-url", e.LocalURL())
|
||||
e.RunCommand("pulumi", "stack", "init", "main-parent")
|
||||
stdout, stderr := e.RunCommandExpectError("pulumi", "up", "--non-interactive", "--skip-preview")
|
||||
assert.Equal(t, "Updating (main-parent):\n", stdout)
|
||||
assert.Equal(t, "Updating (main-parent):\n \n", stdout)
|
||||
assert.Contains(t, stderr, "project 'main' must be a subfolder")
|
||||
e.RunCommand("pulumi", "stack", "rm", "--yes")
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue