feat: Return a specific error type from the printers when resources fail to apply/delete/reconcile

This commit is contained in:
Morten Torkildsen 2022-01-05 09:17:28 -08:00
parent d7d63f4b62
commit 66c63637b4
12 changed files with 562 additions and 187 deletions

View File

@ -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"
}
}

View File

@ -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 {
@ -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
}
@ -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
}
@ -149,15 +55,13 @@ 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
@ -165,10 +69,6 @@ func (b *BaseListPrinter) Print(ch <-chan event.Event, previewStrategy common.Dr
_ = 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
}
@ -180,42 +80,14 @@ 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
}
@ -223,39 +95,14 @@ func (b *BaseListPrinter) Print(ch <-chan event.Event, previewStrategy common.Dr
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

View File

@ -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
}

147
pkg/print/stats/stats.go Normal file
View File

@ -0,0 +1,147 @@
// Copyright 2022 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package stats
import (
"fmt"
"sigs.k8s.io/cli-utils/pkg/apply/event"
)
// Stats captures the summarized numbers from apply/prune/delete and
// reconciliation of resources.
type Stats struct {
ApplyStats ApplyStats
PruneStats PruneStats
DeleteStats DeleteStats
WaitStats WaitStats
}
// FailedActuationSum returns the number of resources that failed actuation.
func (s *Stats) FailedActuationSum() int {
return s.ApplyStats.Failed + s.PruneStats.Failed + s.DeleteStats.Failed
}
// FailedReconciliationSum returns the number of resources that failed reconciliation.
func (s *Stats) FailedReconciliationSum() int {
return s.WaitStats.Failed + s.WaitStats.Timeout
}
// Handle updates the stats based on an event.
func (s *Stats) Handle(e event.Event) {
switch e.Type {
case event.ApplyType:
if e.ApplyEvent.Error != nil {
s.ApplyStats.IncFailed()
return
}
s.ApplyStats.Inc(e.ApplyEvent.Operation)
case event.PruneType:
if e.PruneEvent.Error != nil {
s.PruneStats.IncFailed()
return
}
s.PruneStats.Inc(e.PruneEvent.Operation)
case event.DeleteType:
if e.DeleteEvent.Error != nil {
s.DeleteStats.IncFailed()
return
}
s.DeleteStats.Inc(e.DeleteEvent.Operation)
case event.WaitType:
s.WaitStats.Inc(e.WaitEvent.Operation)
}
}
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) Inc(op event.PruneEventOperation) {
switch op {
case event.PruneUnspecified:
case event.Pruned:
p.Pruned++
case event.PruneSkipped:
p.Skipped++
}
}
func (p *PruneStats) IncFailed() {
p.Failed++
}
type DeleteStats struct {
Deleted int
Skipped int
Failed int
}
func (d *DeleteStats) Inc(op event.DeleteEventOperation) {
switch op {
case event.DeleteUnspecified:
case event.Deleted:
d.Deleted++
case event.DeleteSkipped:
d.Skipped++
}
}
func (d *DeleteStats) IncFailed() {
d.Failed++
}
type WaitStats struct {
Reconciled int
Timeout int
Failed int
Skipped int
}
func (w *WaitStats) Inc(op event.WaitEventOperation) {
switch op {
case event.Reconciled:
w.Reconciled++
case event.ReconcileSkipped:
w.Skipped++
case event.ReconcileTimeout:
w.Timeout++
case event.ReconcileFailed:
w.Failed++
}
}

View File

@ -13,6 +13,7 @@ import (
"sigs.k8s.io/cli-utils/pkg/common"
"sigs.k8s.io/cli-utils/pkg/object"
"sigs.k8s.io/cli-utils/pkg/print/list"
"sigs.k8s.io/cli-utils/pkg/print/stats"
)
func NewFormatter(ioStreams genericclioptions.IOStreams,
@ -107,15 +108,13 @@ func (ef *formatter) FormatErrorEvent(_ event.ErrorEvent) error {
func (ef *formatter) FormatActionGroupEvent(
age event.ActionGroupEvent,
ags []event.ActionGroup,
as *list.ApplyStats,
ps *list.PruneStats,
ds *list.DeleteStats,
ws *list.WaitStats,
s stats.Stats,
_ list.Collector,
) error {
if age.Action == event.ApplyAction &&
age.Type == event.Finished &&
list.IsLastActionGroup(age, ags) {
as := s.ApplyStats
output := fmt.Sprintf("%d resource(s) applied. %d created, %d unchanged, %d configured, %d failed",
as.Sum(), as.Created, as.Unchanged, as.Configured, as.Failed)
// Only print information about serverside apply if some of the
@ -129,19 +128,23 @@ func (ef *formatter) FormatActionGroupEvent(
if age.Action == event.PruneAction &&
age.Type == event.Finished &&
list.IsLastActionGroup(age, ags) {
ef.print("%d resource(s) pruned, %d skipped, %d failed", ps.Pruned, ps.Skipped, ps.Failed)
ps := s.PruneStats
ef.print("%d resource(s) pruned, %d skipped, %d failed to prune", ps.Pruned, ps.Skipped, ps.Failed)
}
if age.Action == event.DeleteAction &&
age.Type == event.Finished &&
list.IsLastActionGroup(age, ags) {
ef.print("%d resource(s) deleted, %d skipped", ds.Deleted, ds.Skipped)
ds := s.DeleteStats
ef.print("%d resource(s) deleted, %d skipped, %d failed to delete", ds.Deleted, ds.Skipped, ds.Failed)
}
if age.Action == event.WaitAction &&
age.Type == event.Finished &&
list.IsLastActionGroup(age, ags) {
ef.print("%d resource(s) reconciled, %d skipped", ws.Reconciled, ds.Skipped)
ws := s.WaitStats
ef.print("%d resource(s) reconciled, %d skipped, %d failed to reconcile, %d timed out", ws.Reconciled,
ws.Skipped, ws.Failed, ws.Timeout)
}
return nil
}

View File

@ -24,7 +24,6 @@ func TestFormatter_FormatApplyEvent(t *testing.T) {
testCases := map[string]struct {
previewStrategy common.DryRunStrategy
event event.ApplyEvent
applyStats *list.ApplyStats
statusCollector list.Collector
expected string
}{
@ -125,7 +124,6 @@ func TestFormatter_FormatPruneEvent(t *testing.T) {
testCases := map[string]struct {
previewStrategy common.DryRunStrategy
event event.PruneEvent
pruneStats *list.PruneStats
expected string
}{
"resource pruned without no dryrun": {
@ -170,7 +168,6 @@ func TestFormatter_FormatDeleteEvent(t *testing.T) {
testCases := map[string]struct {
previewStrategy common.DryRunStrategy
event event.DeleteEvent
deleteStats *list.DeleteStats
statusCollector list.Collector
expected string
}{
@ -219,7 +216,6 @@ func TestFormatter_FormatWaitEvent(t *testing.T) {
testCases := map[string]struct {
previewStrategy common.DryRunStrategy
event event.WaitEvent
waitStats *list.WaitStats
statusCollector list.Collector
expected string
}{

View File

@ -13,6 +13,7 @@ import (
"sigs.k8s.io/cli-utils/pkg/common"
"sigs.k8s.io/cli-utils/pkg/object"
"sigs.k8s.io/cli-utils/pkg/print/list"
"sigs.k8s.io/cli-utils/pkg/print/stats"
)
func NewFormatter(ioStreams genericclioptions.IOStreams,
@ -82,14 +83,12 @@ func (jf *formatter) FormatErrorEvent(ee event.ErrorEvent) error {
func (jf *formatter) FormatActionGroupEvent(
age event.ActionGroupEvent,
ags []event.ActionGroup,
as *list.ApplyStats,
ps *list.PruneStats,
ds *list.DeleteStats,
ws *list.WaitStats,
s stats.Stats,
_ list.Collector,
) error {
if age.Action == event.ApplyAction && age.Type == event.Finished &&
list.IsLastActionGroup(age, ags) {
as := s.ApplyStats
if err := jf.printEvent("apply", "completed", map[string]interface{}{
"count": as.Sum(),
"createdCount": as.Created,
@ -104,22 +103,27 @@ func (jf *formatter) FormatActionGroupEvent(
if age.Action == event.PruneAction && age.Type == event.Finished &&
list.IsLastActionGroup(age, ags) {
ps := s.PruneStats
return jf.printEvent("prune", "completed", map[string]interface{}{
"pruned": ps.Pruned,
"skipped": ps.Skipped,
"failed": ps.Failed,
})
}
if age.Action == event.DeleteAction && age.Type == event.Finished &&
list.IsLastActionGroup(age, ags) {
ds := s.DeleteStats
return jf.printEvent("delete", "completed", map[string]interface{}{
"deleted": ds.Deleted,
"skipped": ds.Skipped,
"failed": ds.Failed,
})
}
if age.Action == event.WaitAction && age.Type == event.Finished &&
list.IsLastActionGroup(age, ags) {
ws := s.WaitStats
return jf.printEvent("wait", "completed", map[string]interface{}{
"reconciled": ws.Reconciled,
"skipped": ws.Skipped,

View File

@ -18,6 +18,7 @@ import (
"sigs.k8s.io/cli-utils/pkg/kstatus/status"
"sigs.k8s.io/cli-utils/pkg/object"
"sigs.k8s.io/cli-utils/pkg/print/list"
"sigs.k8s.io/cli-utils/pkg/print/stats"
)
func TestFormatter_FormatApplyEvent(t *testing.T) {
@ -183,7 +184,6 @@ func TestFormatter_FormatPruneEvent(t *testing.T) {
testCases := map[string]struct {
previewStrategy common.DryRunStrategy
event event.PruneEvent
pruneStats *list.PruneStats
expected map[string]interface{}
}{
"resource pruned without dryrun": {
@ -255,7 +255,6 @@ func TestFormatter_FormatDeleteEvent(t *testing.T) {
testCases := map[string]struct {
previewStrategy common.DryRunStrategy
event event.DeleteEvent
deleteStats *list.DeleteStats
statusCollector list.Collector
expected map[string]interface{}
}{
@ -476,10 +475,7 @@ func TestFormatter_FormatActionGroupEvent(t *testing.T) {
previewStrategy common.DryRunStrategy
event event.ActionGroupEvent
actionGroups []event.ActionGroup
applyStats *list.ApplyStats
pruneStats *list.PruneStats
deleteStats *list.DeleteStats
waitStats *list.WaitStats
statsCollector stats.Stats
statusCollector list.Collector
expected map[string]interface{}
}{
@ -519,8 +515,10 @@ func TestFormatter_FormatActionGroupEvent(t *testing.T) {
Action: event.ApplyAction,
},
},
applyStats: &list.ApplyStats{
ServersideApplied: 42,
statsCollector: stats.Stats{
ApplyStats: stats.ApplyStats{
ServersideApplied: 42,
},
},
expected: map[string]interface{}{
"eventType": "completed",
@ -559,8 +557,7 @@ func TestFormatter_FormatActionGroupEvent(t *testing.T) {
t.Run(tn, func(t *testing.T) {
ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
formatter := NewFormatter(ioStreams, tc.previewStrategy)
err := formatter.FormatActionGroupEvent(tc.event, tc.actionGroups, tc.applyStats, tc.pruneStats,
tc.deleteStats, tc.waitStats, tc.statusCollector)
err := formatter.FormatActionGroupEvent(tc.event, tc.actionGroups, tc.statsCollector, tc.statusCollector)
assert.NoError(t, err)
assertOutput(t, tc.expected, out.String())

View File

@ -12,6 +12,7 @@ import (
pe "sigs.k8s.io/cli-utils/pkg/kstatus/polling/event"
"sigs.k8s.io/cli-utils/pkg/kstatus/status"
"sigs.k8s.io/cli-utils/pkg/object"
"sigs.k8s.io/cli-utils/pkg/print/stats"
"sigs.k8s.io/cli-utils/pkg/print/table"
)
@ -73,6 +74,10 @@ type ResourceInfo struct {
// or Prune.
ResourceAction event.ResourceAction
// Error is set if an error occurred trying to perform
// the desired action on the resource.
Error error
// ApplyOpResult contains the result after
// a resource has been applied to the cluster.
ApplyOpResult event.ApplyEventOperation
@ -211,6 +216,9 @@ func (r *ResourceStateCollector) processApplyEvent(e event.ApplyEvent) {
klog.V(4).Infof("%s apply event not found in ResourceInfos; no processing", identifier)
return
}
if e.Error != nil {
previous.Error = e.Error
}
previous.ApplyOpResult = e.Operation
}
@ -223,6 +231,9 @@ func (r *ResourceStateCollector) processPruneEvent(e event.PruneEvent) {
klog.V(4).Infof("%s prune event not found in ResourceInfos; no processing", identifier)
return
}
if e.Error != nil {
previous.Error = e.Error
}
previous.PruneOpResult = e.Operation
}
@ -285,6 +296,33 @@ func (r *ResourceStateCollector) LatestState() *ResourceState {
}
}
// Stats returns a summary of the results from the actuation operation
// as a stats.Stats object.
func (r *ResourceStateCollector) Stats() stats.Stats {
var s stats.Stats
for _, res := range r.resourceInfos {
switch res.ResourceAction {
case event.ApplyAction:
if res.Error != nil {
s.ApplyStats.IncFailed()
}
s.ApplyStats.Inc(res.ApplyOpResult)
case event.PruneAction:
if res.Error != nil {
s.PruneStats.IncFailed()
}
s.PruneStats.Inc(res.PruneOpResult)
case event.DeleteAction:
if res.Error != nil {
s.DeleteStats.IncFailed()
}
s.DeleteStats.Inc(res.DeleteOpResult)
}
s.WaitStats.Inc(res.WaitOpResult)
}
return s
}
type ResourceInfos []*ResourceInfo
func (g ResourceInfos) Len() int {

View File

@ -11,6 +11,7 @@ import (
"k8s.io/cli-runtime/pkg/genericclioptions"
"sigs.k8s.io/cli-utils/pkg/apply/event"
"sigs.k8s.io/cli-utils/pkg/common"
printcommon "sigs.k8s.io/cli-utils/pkg/print/common"
"sigs.k8s.io/cli-utils/pkg/print/table"
)
@ -18,7 +19,7 @@ type Printer struct {
IOStreams genericclioptions.IOStreams
}
func (t *Printer) Print(ch <-chan event.Event, _ common.DryRunStrategy, printStatus bool) error {
func (t *Printer) Print(ch <-chan event.Event, _ common.DryRunStrategy, _ bool) error {
// Wait for the init event that will give us the set of
// resources.
var initEvent event.InitEvent
@ -61,7 +62,13 @@ func (t *Printer) Print(ch <-chan event.Event, _ common.DryRunStrategy, printSta
// the printer has updated the UI with the latest state and
// exited from the goroutine.
<-printCompleted
return err
if err != nil {
return err
}
// If no fatal errors happened, we will return a ResultError if
// one or more resources failed to apply/prune or reconcile.
return printcommon.ResultErrorFromStats(coll.Stats())
}
// columns defines the columns we want to print

View File

@ -7,8 +7,11 @@ import (
"bytes"
"testing"
"k8s.io/cli-runtime/pkg/genericclioptions"
"sigs.k8s.io/cli-utils/pkg/apply/event"
"sigs.k8s.io/cli-utils/pkg/print/table"
"sigs.k8s.io/cli-utils/pkg/printers/printer"
printertesting "sigs.k8s.io/cli-utils/pkg/printers/testutil"
)
var (
@ -72,3 +75,12 @@ func TestActionColumnDef(t *testing.T) {
})
}
}
func TestPrint(t *testing.T) {
printertesting.PrintResultErrorTest(t, func() printer.Printer {
ioStreams, _, _, _ := genericclioptions.NewTestIOStreams()
return &Printer{
IOStreams: ioStreams,
}
})
}

View File

@ -0,0 +1,205 @@
// Copyright 2022 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package testutil
import (
"fmt"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/runtime/schema"
"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"
"sigs.k8s.io/cli-utils/pkg/printers/printer"
)
type PrinterFactoryFunc func() printer.Printer
func PrintResultErrorTest(t *testing.T, f PrinterFactoryFunc) {
deploymentIdentifier := object.ObjMetadata{
GroupKind: schema.GroupKind{
Group: "apps",
Kind: "Deployment",
},
Name: "foo",
Namespace: "bar",
}
testCases := map[string]struct {
events []event.Event
expectedErr error
}{
"successful apply, prune and reconcile": {
events: []event.Event{
{
Type: event.InitType,
InitEvent: event.InitEvent{
ActionGroups: event.ActionGroupList{
{
Name: "apply-1",
Action: event.ApplyAction,
Identifiers: []object.ObjMetadata{
deploymentIdentifier,
},
},
{
Name: "wait-1",
Action: event.WaitAction,
Identifiers: []object.ObjMetadata{
deploymentIdentifier,
},
},
},
},
},
{
Type: event.ApplyType,
ApplyEvent: event.ApplyEvent{
Operation: event.Created,
Identifier: deploymentIdentifier,
},
},
{
Type: event.WaitType,
WaitEvent: event.WaitEvent{
Operation: event.Reconciled,
Identifier: deploymentIdentifier,
},
},
},
expectedErr: nil,
},
"successful apply, failed reconcile": {
events: []event.Event{
{
Type: event.InitType,
InitEvent: event.InitEvent{
ActionGroups: event.ActionGroupList{
{
Name: "apply-1",
Action: event.ApplyAction,
Identifiers: []object.ObjMetadata{
deploymentIdentifier,
},
},
{
Name: "wait-1",
Action: event.WaitAction,
Identifiers: []object.ObjMetadata{
deploymentIdentifier,
},
},
},
},
},
{
Type: event.ApplyType,
ApplyEvent: event.ApplyEvent{
Operation: event.Created,
Identifier: deploymentIdentifier,
},
},
{
Type: event.WaitType,
WaitEvent: event.WaitEvent{
Operation: event.ReconcileFailed,
Identifier: deploymentIdentifier,
},
},
},
expectedErr: &printcommon.ResultError{
Stats: stats.Stats{
ApplyStats: stats.ApplyStats{
Created: 1,
},
WaitStats: stats.WaitStats{
Failed: 1,
},
},
},
},
"failed apply": {
events: []event.Event{
{
Type: event.InitType,
InitEvent: event.InitEvent{
ActionGroups: event.ActionGroupList{
{
Name: "apply-1",
Action: event.ApplyAction,
Identifiers: []object.ObjMetadata{
deploymentIdentifier,
},
},
{
Name: "wait-1",
Action: event.WaitAction,
Identifiers: []object.ObjMetadata{
deploymentIdentifier,
},
},
},
},
},
{
Type: event.ApplyType,
ApplyEvent: event.ApplyEvent{
Operation: event.ApplyUnspecified,
Identifier: deploymentIdentifier,
Error: fmt.Errorf("apply failed"),
},
},
{
Type: event.WaitType,
WaitEvent: event.WaitEvent{
Operation: event.ReconcileSkipped,
Identifier: deploymentIdentifier,
},
},
},
expectedErr: &printcommon.ResultError{
Stats: stats.Stats{
ApplyStats: stats.ApplyStats{
Failed: 1,
},
WaitStats: stats.WaitStats{
Skipped: 1,
},
},
},
},
}
for tn := range testCases {
tc := testCases[tn]
t.Run(tn, func(t *testing.T) {
p := f()
eventChannel := make(chan event.Event)
var wg sync.WaitGroup
var err error
wg.Add(1)
go func() {
err = p.Print(eventChannel, common.DryRunNone, false)
wg.Done()
}()
for i := range tc.events {
e := tc.events[i]
eventChannel <- e
}
close(eventChannel)
wg.Wait()
assert.Equal(t, tc.expectedErr, err)
})
}
}