Skip to content

Commit

Permalink
Merge pull request #500 from mortent/ApplyReconcileFailedError
Browse files Browse the repository at this point in the history
feat: Return a specific error type from the printers when resources fail to apply/delete/reconcile
  • Loading branch information
k8s-ci-robot authored Jan 11, 2022
2 parents d7d63f4 + 66c6363 commit 0c9b214
Show file tree
Hide file tree
Showing 12 changed files with 562 additions and 187 deletions.
45 changes: 45 additions & 0 deletions pkg/print/common/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright 2022 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0

package common

import (
"fmt"

"sigs.k8s.io/cli-utils/pkg/print/stats"
)

// ResultErrorFromStats takes a stats object and returns either a ResultError or
// nil depending on whether the stats reports that resources failed apply/prune/delete
// or reconciliation.
func ResultErrorFromStats(s stats.Stats) error {
if s.FailedActuationSum() > 0 || s.FailedReconciliationSum() > 0 {
return &ResultError{
Stats: s,
}
}
return nil
}

// ResultError is returned from printers when the apply/destroy operations completed, but one or
// more resources either failed apply/prune/delete, or failed to reconcile.
type ResultError struct {
Stats stats.Stats
}

func (a *ResultError) Error() string {
switch {
case a.Stats.FailedActuationSum() > 0 && a.Stats.FailedReconciliationSum() > 0:
return fmt.Sprintf("%d resources failed, %d resources failed to reconcile before timeout",
a.Stats.FailedActuationSum(), a.Stats.FailedReconciliationSum())
case a.Stats.FailedActuationSum() > 0:
return fmt.Sprintf("%d resources failed", a.Stats.FailedActuationSum())
case a.Stats.FailedReconciliationSum() > 0:
return fmt.Sprintf("%d resources failed to reconcile before timeout",
a.Stats.FailedReconciliationSum())
default:
// Should not happen as this error is only used when at least one resource
// either failed to apply/prune/delete or reconcile.
return "unknown error"
}
}
167 changes: 7 additions & 160 deletions pkg/print/list/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
package list

import (
"fmt"

"sigs.k8s.io/cli-utils/pkg/apply/event"
"sigs.k8s.io/cli-utils/pkg/common"
"sigs.k8s.io/cli-utils/pkg/object"
printcommon "sigs.k8s.io/cli-utils/pkg/print/common"
"sigs.k8s.io/cli-utils/pkg/print/stats"
)

type Formatter interface {
Expand All @@ -21,10 +21,7 @@ type Formatter interface {
FormatActionGroupEvent(
age event.ActionGroupEvent,
ags []event.ActionGroup,
as *ApplyStats,
ps *PruneStats,
ds *DeleteStats,
ws *WaitStats,
s stats.Stats,
c Collector,
) error
}
Expand All @@ -35,97 +32,6 @@ type BaseListPrinter struct {
FormatterFactory FormatterFactory
}

type ApplyStats struct {
ServersideApplied int
Created int
Unchanged int
Configured int
Failed int
}

func (a *ApplyStats) inc(op event.ApplyEventOperation) {
switch op {
case event.ApplyUnspecified:
case event.ServersideApplied:
a.ServersideApplied++
case event.Created:
a.Created++
case event.Unchanged:
a.Unchanged++
case event.Configured:
a.Configured++
default:
panic(fmt.Errorf("unknown apply operation %s", op.String()))
}
}

func (a *ApplyStats) incFailed() {
a.Failed++
}

func (a *ApplyStats) Sum() int {
return a.ServersideApplied + a.Configured + a.Unchanged + a.Created + a.Failed
}

type PruneStats struct {
Pruned int
Skipped int
Failed int
}

func (p *PruneStats) incPruned() {
p.Pruned++
}

func (p *PruneStats) incSkipped() {
p.Skipped++
}

func (p *PruneStats) incFailed() {
p.Failed++
}

type DeleteStats struct {
Deleted int
Skipped int
Failed int
}

func (d *DeleteStats) incDeleted() {
d.Deleted++
}

func (d *DeleteStats) incSkipped() {
d.Skipped++
}

func (d *DeleteStats) incFailed() {
d.Failed++
}

type WaitStats struct {
Reconciled int
Timeout int
Failed int
Skipped int
}

func (w *WaitStats) incReconciled() {
w.Reconciled++
}

func (w *WaitStats) incTimeout() {
w.Timeout++
}

func (w *WaitStats) incFailed() {
w.Failed++
}

func (w *WaitStats) incSkipped() {
w.Skipped++
}

type Collector interface {
LatestStatus() map[object.ObjMetadata]event.StatusEvent
}
Expand All @@ -149,26 +55,20 @@ func (sc *StatusCollector) LatestStatus() map[object.ObjMetadata]event.StatusEve
//nolint:gocyclo
func (b *BaseListPrinter) Print(ch <-chan event.Event, previewStrategy common.DryRunStrategy, printStatus bool) error {
var actionGroups []event.ActionGroup
applyStats := &ApplyStats{}
pruneStats := &PruneStats{}
deleteStats := &DeleteStats{}
waitStats := &WaitStats{}
var statsCollector stats.Stats
statusCollector := &StatusCollector{
latestStatus: make(map[object.ObjMetadata]event.StatusEvent),
}
formatter := b.FormatterFactory(previewStrategy)
for e := range ch {
statsCollector.Handle(e)
switch e.Type {
case event.InitType:
actionGroups = e.InitEvent.ActionGroups
case event.ErrorType:
_ = formatter.FormatErrorEvent(e.ErrorEvent)
return e.ErrorEvent.Err
case event.ApplyType:
applyStats.inc(e.ApplyEvent.Operation)
if e.ApplyEvent.Error != nil {
applyStats.incFailed()
}
if err := formatter.FormatApplyEvent(e.ApplyEvent); err != nil {
return err
}
Expand All @@ -180,82 +80,29 @@ func (b *BaseListPrinter) Print(ch <-chan event.Event, previewStrategy common.Dr
}
}
case event.PruneType:
switch e.PruneEvent.Operation {
case event.Pruned:
pruneStats.incPruned()
case event.PruneSkipped:
pruneStats.incSkipped()
}
if e.PruneEvent.Error != nil {
pruneStats.incFailed()
}
if err := formatter.FormatPruneEvent(e.PruneEvent); err != nil {
return err
}
case event.DeleteType:
switch e.DeleteEvent.Operation {
case event.Deleted:
deleteStats.incDeleted()
case event.DeleteSkipped:
deleteStats.incSkipped()
}
if e.DeleteEvent.Error != nil {
deleteStats.incFailed()
}
if err := formatter.FormatDeleteEvent(e.DeleteEvent); err != nil {
return err
}
case event.WaitType:
switch e.WaitEvent.Operation {
case event.Reconciled:
waitStats.incReconciled()
case event.ReconcileSkipped:
waitStats.incSkipped()
case event.ReconcileTimeout:
waitStats.incTimeout()
case event.ReconcileFailed:
waitStats.incFailed()
}
if err := formatter.FormatWaitEvent(e.WaitEvent); err != nil {
return err
}
case event.ActionGroupType:
if err := formatter.FormatActionGroupEvent(
e.ActionGroupEvent,
actionGroups,
applyStats,
pruneStats,
deleteStats,
waitStats,
statsCollector,
statusCollector,
); err != nil {
return err
}
}
}
failedActuateSum := applyStats.Failed + pruneStats.Failed + deleteStats.Failed
failedReconcileSum := waitStats.Timeout + waitStats.Failed
switch {
case failedActuateSum > 0 && failedReconcileSum > 0:
return fmt.Errorf("%d resources failed, %d resources failed to reconcile before timeout",
failedActuateSum, failedReconcileSum)
case failedActuateSum > 0:
return fmt.Errorf("%d resources failed", failedActuateSum)
case failedReconcileSum > 0:
return fmt.Errorf("%d resources failed to reconcile before timeout",
failedReconcileSum)
default:
return nil
}
}

func ActionGroupByName(name string, ags []event.ActionGroup) (event.ActionGroup, bool) {
for _, ag := range ags {
if ag.Name == name {
return ag, true
}
}
return event.ActionGroup{}, false
return printcommon.ResultErrorFromStats(statsCollector)
}

// IsLastActionGroup returns true if the passed ActionGroupEvent is the
Expand Down
74 changes: 74 additions & 0 deletions pkg/print/list/base_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright 2022 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0

package list

import (
"testing"

"sigs.k8s.io/cli-utils/pkg/apply/event"
"sigs.k8s.io/cli-utils/pkg/common"
"sigs.k8s.io/cli-utils/pkg/print/stats"
"sigs.k8s.io/cli-utils/pkg/printers/printer"
printertesting "sigs.k8s.io/cli-utils/pkg/printers/testutil"
)

func TestPrint(t *testing.T) {
printertesting.PrintResultErrorTest(t, func() printer.Printer {
return &BaseListPrinter{
FormatterFactory: func(previewStrategy common.DryRunStrategy) Formatter {
return newCountingFormatter()
},
}
})
}

func newCountingFormatter() *countingFormatter {
return &countingFormatter{}
}

type countingFormatter struct {
applyEvents []event.ApplyEvent
statusEvents []event.StatusEvent
pruneEvents []event.PruneEvent
deleteEvents []event.DeleteEvent
waitEvents []event.WaitEvent
errorEvent event.ErrorEvent
actionGroupEvent []event.ActionGroupEvent
}

func (c *countingFormatter) FormatApplyEvent(e event.ApplyEvent) error {
c.applyEvents = append(c.applyEvents, e)
return nil
}

func (c *countingFormatter) FormatStatusEvent(e event.StatusEvent) error {
c.statusEvents = append(c.statusEvents, e)
return nil
}

func (c *countingFormatter) FormatPruneEvent(e event.PruneEvent) error {
c.pruneEvents = append(c.pruneEvents, e)
return nil
}

func (c *countingFormatter) FormatDeleteEvent(e event.DeleteEvent) error {
c.deleteEvents = append(c.deleteEvents, e)
return nil
}

func (c *countingFormatter) FormatWaitEvent(e event.WaitEvent) error {
c.waitEvents = append(c.waitEvents, e)
return nil
}

func (c *countingFormatter) FormatErrorEvent(e event.ErrorEvent) error {
c.errorEvent = e
return nil
}

func (c *countingFormatter) FormatActionGroupEvent(e event.ActionGroupEvent, _ []event.ActionGroup, _ stats.Stats,
_ Collector) error {
c.actionGroupEvent = append(c.actionGroupEvent, e)
return nil
}
Loading

0 comments on commit 0c9b214

Please sign in to comment.