mirror of https://github.com/fluxcd/cli-utils.git
476 lines
10 KiB
Go
476 lines
10 KiB
Go
// Copyright 2020 The Kubernetes Authors.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package testutil
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"sigs.k8s.io/cli-utils/pkg/apply/event"
|
|
"sigs.k8s.io/cli-utils/pkg/kstatus/status"
|
|
"sigs.k8s.io/cli-utils/pkg/object"
|
|
)
|
|
|
|
type ExpEvent struct {
|
|
EventType event.Type
|
|
|
|
InitEvent *ExpInitEvent
|
|
ErrorEvent *ExpErrorEvent
|
|
ActionGroupEvent *ExpActionGroupEvent
|
|
ApplyEvent *ExpApplyEvent
|
|
StatusEvent *ExpStatusEvent
|
|
PruneEvent *ExpPruneEvent
|
|
DeleteEvent *ExpDeleteEvent
|
|
WaitEvent *ExpWaitEvent
|
|
ValidationEvent *ExpValidationEvent
|
|
}
|
|
|
|
type ExpInitEvent struct {
|
|
// TODO: enable if we want to more thuroughly test InitEvents
|
|
// ActionGroups []event.ActionGroup
|
|
}
|
|
|
|
type ExpErrorEvent struct {
|
|
Err error
|
|
}
|
|
|
|
type ExpActionGroupEvent struct {
|
|
GroupName string
|
|
Action event.ResourceAction
|
|
Type event.ActionGroupEventStatus
|
|
}
|
|
|
|
type ExpApplyEvent struct {
|
|
GroupName string
|
|
Status event.ApplyEventStatus
|
|
Identifier object.ObjMetadata
|
|
Error error
|
|
}
|
|
|
|
type ExpStatusEvent struct {
|
|
Status status.Status
|
|
Identifier object.ObjMetadata
|
|
Error error
|
|
}
|
|
|
|
type ExpPruneEvent struct {
|
|
GroupName string
|
|
Status event.PruneEventStatus
|
|
Identifier object.ObjMetadata
|
|
Error error
|
|
}
|
|
|
|
type ExpDeleteEvent struct {
|
|
GroupName string
|
|
Status event.DeleteEventStatus
|
|
Identifier object.ObjMetadata
|
|
Error error
|
|
}
|
|
|
|
type ExpWaitEvent struct {
|
|
GroupName string
|
|
Status event.WaitEventStatus
|
|
Identifier object.ObjMetadata
|
|
}
|
|
|
|
type ExpValidationEvent struct {
|
|
Identifiers object.ObjMetadataSet
|
|
Error error
|
|
}
|
|
|
|
func VerifyEvents(expEvents []ExpEvent, events []event.Event) error {
|
|
if len(expEvents) == 0 && len(events) == 0 {
|
|
return nil
|
|
}
|
|
expEventIndex := 0
|
|
for i := range events {
|
|
e := events[i]
|
|
ee := expEvents[expEventIndex]
|
|
if isMatch(ee, e) {
|
|
expEventIndex++
|
|
if expEventIndex >= len(expEvents) {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
return fmt.Errorf("event %s not found", expEvents[expEventIndex].EventType)
|
|
}
|
|
|
|
// nolint:gocyclo
|
|
// TODO(mortent): This function is pretty complex and with quite a bit of
|
|
// duplication. We should see if there is a better way to provide a flexible
|
|
// way to verify that we go the expected events.
|
|
func isMatch(ee ExpEvent, e event.Event) bool {
|
|
if ee.EventType != e.Type {
|
|
return false
|
|
}
|
|
|
|
// nolint:gocritic
|
|
switch e.Type {
|
|
case event.ErrorType:
|
|
a := ee.ErrorEvent
|
|
|
|
if a == nil {
|
|
return true
|
|
}
|
|
|
|
b := e.ErrorEvent
|
|
|
|
if a.Err != nil {
|
|
if !cmp.Equal(a.Err, b.Err, cmpopts.EquateErrors()) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
|
|
case event.ActionGroupType:
|
|
agee := ee.ActionGroupEvent
|
|
|
|
if agee == nil {
|
|
return true
|
|
}
|
|
|
|
age := e.ActionGroupEvent
|
|
|
|
if agee.GroupName != age.GroupName {
|
|
return false
|
|
}
|
|
|
|
if agee.Action != age.Action {
|
|
return false
|
|
}
|
|
|
|
if agee.Type != age.Status {
|
|
return false
|
|
}
|
|
return true
|
|
|
|
case event.ApplyType:
|
|
aee := ee.ApplyEvent
|
|
// If no more information is specified, we consider it a match.
|
|
if aee == nil {
|
|
return true
|
|
}
|
|
ae := e.ApplyEvent
|
|
|
|
if aee.Identifier != object.NilObjMetadata {
|
|
if aee.Identifier != ae.Identifier {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if aee.GroupName != "" {
|
|
if aee.GroupName != ae.GroupName {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if aee.Status != ae.Status {
|
|
return false
|
|
}
|
|
|
|
if aee.Error != nil {
|
|
return ae.Error != nil
|
|
}
|
|
return ae.Error == nil
|
|
|
|
case event.StatusType:
|
|
see := ee.StatusEvent
|
|
if see == nil {
|
|
return true
|
|
}
|
|
se := e.StatusEvent
|
|
|
|
if see.Identifier != se.Identifier {
|
|
return false
|
|
}
|
|
|
|
if see.Status != se.PollResourceInfo.Status {
|
|
return false
|
|
}
|
|
|
|
if see.Error != nil {
|
|
return se.Error != nil
|
|
}
|
|
return se.Error == nil
|
|
|
|
case event.PruneType:
|
|
pee := ee.PruneEvent
|
|
if pee == nil {
|
|
return true
|
|
}
|
|
pe := e.PruneEvent
|
|
|
|
if pee.Identifier != object.NilObjMetadata {
|
|
if pee.Identifier != pe.Identifier {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if pee.GroupName != "" {
|
|
if pee.GroupName != pe.GroupName {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if pee.Status != pe.Status {
|
|
return false
|
|
}
|
|
|
|
if pee.Error != nil {
|
|
return pe.Error != nil
|
|
}
|
|
return pe.Error == nil
|
|
|
|
case event.DeleteType:
|
|
dee := ee.DeleteEvent
|
|
if dee == nil {
|
|
return true
|
|
}
|
|
de := e.DeleteEvent
|
|
|
|
if dee.Identifier != object.NilObjMetadata {
|
|
if dee.Identifier != de.Identifier {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if dee.GroupName != "" {
|
|
if dee.GroupName != de.GroupName {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if dee.Status != de.Status {
|
|
return false
|
|
}
|
|
|
|
if dee.Error != nil {
|
|
return de.Error != nil
|
|
}
|
|
return de.Error == nil
|
|
|
|
case event.WaitType:
|
|
wee := ee.WaitEvent
|
|
if wee == nil {
|
|
return true
|
|
}
|
|
we := e.WaitEvent
|
|
|
|
if wee.Identifier != object.NilObjMetadata {
|
|
if wee.Identifier != we.Identifier {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if wee.GroupName != "" {
|
|
if wee.GroupName != we.GroupName {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if wee.Status != we.Status {
|
|
return false
|
|
}
|
|
return true
|
|
|
|
case event.ValidationType:
|
|
vee := ee.ValidationEvent
|
|
if vee == nil {
|
|
return true
|
|
}
|
|
ve := e.ValidationEvent
|
|
|
|
if vee.Identifiers != nil {
|
|
if !vee.Identifiers.Equal(ve.Identifiers) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if vee.Error != nil {
|
|
return ve.Error != nil
|
|
}
|
|
return ve.Error == nil
|
|
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
func EventsToExpEvents(events []event.Event) []ExpEvent {
|
|
result := make([]ExpEvent, 0, len(events))
|
|
for _, event := range events {
|
|
result = append(result, EventToExpEvent(event))
|
|
}
|
|
return result
|
|
}
|
|
|
|
func EventToExpEvent(e event.Event) ExpEvent {
|
|
switch e.Type {
|
|
case event.InitType:
|
|
return ExpEvent{
|
|
EventType: event.InitType,
|
|
InitEvent: &ExpInitEvent{
|
|
// TODO: enable if we want to more thuroughly test InitEvents
|
|
// ActionGroups: e.InitEvent.ActionGroups,
|
|
},
|
|
}
|
|
|
|
case event.ErrorType:
|
|
return ExpEvent{
|
|
EventType: event.ErrorType,
|
|
ErrorEvent: &ExpErrorEvent{
|
|
Err: e.ErrorEvent.Err,
|
|
},
|
|
}
|
|
|
|
case event.ActionGroupType:
|
|
return ExpEvent{
|
|
EventType: event.ActionGroupType,
|
|
ActionGroupEvent: &ExpActionGroupEvent{
|
|
GroupName: e.ActionGroupEvent.GroupName,
|
|
Action: e.ActionGroupEvent.Action,
|
|
Type: e.ActionGroupEvent.Status,
|
|
},
|
|
}
|
|
|
|
case event.ApplyType:
|
|
return ExpEvent{
|
|
EventType: event.ApplyType,
|
|
ApplyEvent: &ExpApplyEvent{
|
|
GroupName: e.ApplyEvent.GroupName,
|
|
Identifier: e.ApplyEvent.Identifier,
|
|
Status: e.ApplyEvent.Status,
|
|
Error: e.ApplyEvent.Error,
|
|
},
|
|
}
|
|
|
|
case event.StatusType:
|
|
return ExpEvent{
|
|
EventType: event.StatusType,
|
|
StatusEvent: &ExpStatusEvent{
|
|
Identifier: e.StatusEvent.Identifier,
|
|
Status: e.StatusEvent.PollResourceInfo.Status,
|
|
Error: e.StatusEvent.Error,
|
|
},
|
|
}
|
|
|
|
case event.PruneType:
|
|
return ExpEvent{
|
|
EventType: event.PruneType,
|
|
PruneEvent: &ExpPruneEvent{
|
|
GroupName: e.PruneEvent.GroupName,
|
|
Identifier: e.PruneEvent.Identifier,
|
|
Status: e.PruneEvent.Status,
|
|
Error: e.PruneEvent.Error,
|
|
},
|
|
}
|
|
|
|
case event.DeleteType:
|
|
return ExpEvent{
|
|
EventType: event.DeleteType,
|
|
DeleteEvent: &ExpDeleteEvent{
|
|
GroupName: e.DeleteEvent.GroupName,
|
|
Identifier: e.DeleteEvent.Identifier,
|
|
Status: e.DeleteEvent.Status,
|
|
Error: e.DeleteEvent.Error,
|
|
},
|
|
}
|
|
|
|
case event.WaitType:
|
|
return ExpEvent{
|
|
EventType: event.WaitType,
|
|
WaitEvent: &ExpWaitEvent{
|
|
GroupName: e.WaitEvent.GroupName,
|
|
Identifier: e.WaitEvent.Identifier,
|
|
Status: e.WaitEvent.Status,
|
|
},
|
|
}
|
|
|
|
case event.ValidationType:
|
|
return ExpEvent{
|
|
EventType: event.ValidationType,
|
|
ValidationEvent: &ExpValidationEvent{
|
|
Identifiers: e.ValidationEvent.Identifiers,
|
|
Error: e.ValidationEvent.Error,
|
|
},
|
|
}
|
|
}
|
|
return ExpEvent{}
|
|
}
|
|
|
|
func RemoveEqualEvents(in []ExpEvent, expected ExpEvent) ([]ExpEvent, int) {
|
|
matches := 0
|
|
for i := 0; i < len(in); i++ {
|
|
if cmp.Equal(in[i], expected, cmpopts.EquateErrors()) {
|
|
// remove event at index i
|
|
in = append(in[:i], in[i+1:]...)
|
|
matches++
|
|
i--
|
|
}
|
|
}
|
|
return in, matches
|
|
}
|
|
|
|
// GroupedEventsByID implements sort.Interface for []ExpEvent based on
|
|
// the serialized ObjMetadata of Apply, Prune, and Delete events within the same
|
|
// task group.
|
|
// This makes testing events easier, because apply/prune/delete order is
|
|
// non-deterministic within each task group.
|
|
// This is only needed if you expect to have multiple apply/prune/delete events
|
|
// in the same task group.
|
|
type GroupedEventsByID []ExpEvent
|
|
|
|
func (ape GroupedEventsByID) Len() int { return len(ape) }
|
|
func (ape GroupedEventsByID) Swap(i, j int) { ape[i], ape[j] = ape[j], ape[i] }
|
|
func (ape GroupedEventsByID) Less(i, j int) bool {
|
|
if ape[i].EventType != ape[j].EventType {
|
|
// don't change order if not the same type
|
|
return false
|
|
}
|
|
switch ape[i].EventType {
|
|
case event.ApplyType:
|
|
if ape[i].ApplyEvent.GroupName != ape[j].ApplyEvent.GroupName {
|
|
// don't change order if not the same task group
|
|
return false
|
|
}
|
|
return ape[i].ApplyEvent.Identifier.String() < ape[j].ApplyEvent.Identifier.String()
|
|
case event.PruneType:
|
|
if ape[i].PruneEvent.GroupName != ape[j].PruneEvent.GroupName {
|
|
// don't change order if not the same task group
|
|
return false
|
|
}
|
|
return ape[i].PruneEvent.Identifier.String() < ape[j].PruneEvent.Identifier.String()
|
|
case event.DeleteType:
|
|
if ape[i].DeleteEvent.GroupName != ape[j].DeleteEvent.GroupName {
|
|
// don't change order if not the same task group
|
|
return false
|
|
}
|
|
return ape[i].DeleteEvent.Identifier.String() < ape[j].DeleteEvent.Identifier.String()
|
|
case event.WaitType:
|
|
if ape[i].WaitEvent.GroupName != ape[j].WaitEvent.GroupName {
|
|
// don't change order if not the same task group
|
|
return false
|
|
}
|
|
if ape[i].WaitEvent.Status != ape[j].WaitEvent.Status {
|
|
// don't change order if not the same status
|
|
return false
|
|
}
|
|
if ape[i].WaitEvent.Status != event.ReconcileSuccessful {
|
|
// pending, skipped, and timeout status are predictably ordered
|
|
// using the order in WaitTask.Ids.
|
|
// So we only need to sort Reconciled events, which occur in the
|
|
// order the Waitask receives StatusEvents with Current/NotFound.
|
|
return false
|
|
}
|
|
return ape[i].WaitEvent.Identifier.String() < ape[j].WaitEvent.Identifier.String()
|
|
case event.ValidationType:
|
|
return ape[i].ValidationEvent.Identifiers.Hash() < ape[j].ValidationEvent.Identifiers.Hash()
|
|
default:
|
|
// don't change order if not ApplyType, PruneType, or DeleteType
|
|
return false
|
|
}
|
|
}
|