mirror of https://github.com/fluxcd/cli-utils.git
feat: Return a specific error type from the printers when resources fail to apply/delete/reconcile
This commit is contained in:
parent
d7d63f4b62
commit
66c63637b4
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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++
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}{
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue