From 76efa28959e13be3d11fe39cebe05a2e439a0ea3 Mon Sep 17 00:00:00 2001 From: Jack Ottofaro Date: Wed, 27 Jul 2022 17:13:37 -0400 Subject: [PATCH] /pkg/cvo: improve CV history pruning by implementing enhancement [1]. [1]: https://github.com/openshift/enhancements/blob/master/enhancements/update/clusterversion-history-pruning.md --- pkg/cvo/status.go | 25 +- pkg/cvo/status_history.go | 217 +++++++++ pkg/cvo/status_history_test.go | 777 +++++++++++++++++++++++++++++++++ pkg/cvo/status_test.go | 148 ------- 4 files changed, 999 insertions(+), 168 deletions(-) create mode 100644 pkg/cvo/status_history.go create mode 100644 pkg/cvo/status_history_test.go diff --git a/pkg/cvo/status.go b/pkg/cvo/status.go index 08934bc5cf..4294be5ad8 100644 --- a/pkg/cvo/status.go +++ b/pkg/cvo/status.go @@ -31,8 +31,9 @@ const ( ClusterStatusFailing = configv1.ClusterStatusConditionType("Failing") // MaxHistory is the maximum size of ClusterVersion history. Once exceeded - // ClusterVersion history will be pruned. - MaxHistory = 50 + // ClusterVersion history will be pruned. It is declared here and passed + // into the pruner function to allow easier testing. + MaxHistory = 100 ) func mergeEqualVersions(current *configv1.UpdateHistory, desired configv1.Release) bool { @@ -131,28 +132,12 @@ func mergeOperatorHistory(config *configv1.ClusterVersion, desired configv1.Rele config.Status.History[0].Verified = true } - // TODO: prune Z versions over transitions to Y versions, keep initial installed version - pruneStatusHistory(config, MaxHistory) + // Prune least informative history entry when at maxHistory. + prune(config.Status.History, MaxHistory) config.Status.Desired = desired } -// pruneStatusHistory maintains history size at MaxHistory by removing entry at index MaxHistory -// unless that entry is a completed update in which case entry at MaxHistory-1 is removed thereby -// retaining the initial completed version. -func pruneStatusHistory(config *configv1.ClusterVersion, maxHistory int) { - if len(config.Status.History) <= maxHistory { - return - } - if config.Status.History[maxHistory].State == configv1.CompletedUpdate { - item := config.Status.History[maxHistory] - config.Status.History = config.Status.History[0 : maxHistory-1] - config.Status.History = append(config.Status.History, item) - } else { - config.Status.History = config.Status.History[:maxHistory] - } -} - // ClusterVersionInvalid indicates that the cluster version has an error that prevents the server from // taking action. The cluster version operator will only reconcile the current state as long as this // condition is set. diff --git a/pkg/cvo/status_history.go b/pkg/cvo/status_history.go new file mode 100644 index 0000000000..9319744c33 --- /dev/null +++ b/pkg/cvo/status_history.go @@ -0,0 +1,217 @@ +package cvo + +import ( + "math" + "strings" + + "k8s.io/klog/v2" + + configv1 "github.com/openshift/api/config/v1" +) + +const ( + // maxFinalEntryIndex is an upper bound index. History entries from 0 to maxFinalEntryIndex are given mostImportantWeight. + maxFinalEntryIndex = 4 + + // mostImportantWeight is given to history entries that should never be removed including the initial entry, + // the most recent entries (indices 0..maxFinalEntryIndex), and the most recently completed update. + mostImportantWeight = 1000.0 + + // interestingWeight is given to the first or last completed update in a given minor. These are interesting + // but not critical. + interestingWeight = 30.0 + + // partialMinorWeight is given to any partial minor updates between a completed minor version transition. + partialMinorWeight = 20.0 + + // partialZStreamWeight is given to any partial z-stream updates between a completed z-stream version transition. + partialZStreamWeight = -20.0 + + // sliceIndexWeight when applied will favor more recent updates and avoid ties. + sliceIndexWeight = -1.01 +) + +// prune prunes history when at maxSize by ranking each entry using the above defined weights and removing the entry with +// the lowest rank. maxSize is passed in to allow the use of smaller values which eases unit test. +func prune(history []configv1.UpdateHistory, maxSize int) []configv1.UpdateHistory { + if len(history) <= maxSize { + return history + } + mostRecentCompletedEntryIndex := getTheMostRecentCompletedEntryIndex(history) + + var reason string + lowestRank := math.MaxFloat64 + var lowestRankIdx int + + for i := range history { + rank := 0.0 + thisReason := "an older update of less importance" + if isTheInitialEntry(i, maxSize) || isAFinalEntry(i) || isTheMostRecentCompletedEntry(i, mostRecentCompletedEntryIndex) { + rank = mostImportantWeight + } else if isTheFirstOrLastCompletedInAMinor(i, history, maxSize) { + rank = rank + interestingWeight + thisReason = "the first or last completed minor update" + } else if isPartialPortionOfMinorTransition(i, history, maxSize) { + rank = rank + partialMinorWeight + thisReason = "a partial update within a minor transition" + } else if isPartialWithinAZStream(i, history, maxSize) { + rank = rank + partialZStreamWeight + thisReason = "a partial update within a z-stream transition" + } + rank += sliceIndexWeight * float64(i) + + if rank < lowestRank { + lowestRank = rank + lowestRankIdx = i + reason = thisReason + } + } + klog.V(2).Infof("Pruning %s version %s at index %d with rank %f.", reason, history[lowestRankIdx].Version, lowestRankIdx, lowestRank) + + var prunedHistory []configv1.UpdateHistory + if lowestRankIdx == maxSize { + prunedHistory = history[:maxSize] + } else { + prunedHistory = append(history[:lowestRankIdx], history[lowestRankIdx+1:]...) + } + return prunedHistory +} + +// isTheInitialEntry returns true if entryIndex is the first entry entered into the slice (i.e. is maxHistorySize). +func isTheInitialEntry(entryIndex int, maxHistorySize int) bool { + return entryIndex == maxHistorySize +} + +// isAFinalEntry returns true if entryIndex falls within 0..maxFinalEntryIndex. +func isAFinalEntry(entryIndex int) bool { + return entryIndex <= maxFinalEntryIndex +} + +// isTheMostRecentCompletedEntry returns true if entryIndex is equal to theMostRecentCompletedEntryIndex. +func isTheMostRecentCompletedEntry(entryIndex int, theMostRecentCompletedEntryIndex int) bool { + return entryIndex == theMostRecentCompletedEntryIndex +} + +// isTheFirstOrLastCompletedInAMinor returns true if the entry at entryIndex is the first or last completed update for +// a given minor version change. +func isTheFirstOrLastCompletedInAMinor(entryIndex int, h []configv1.UpdateHistory, maxHistorySize int) bool { + if h[entryIndex].State == configv1.PartialUpdate { + return false + } + if entryIndex == 0 || entryIndex == maxHistorySize { + return true + } + nextIdx := findNextOlderCompleted(entryIndex, h) + if nextIdx == entryIndex || !sameMinorVersion(h[entryIndex], h[nextIdx]) { + return true + } + nextIdx = findNextNewerCompleted(entryIndex, h) + if nextIdx == entryIndex || !sameMinorVersion(h[entryIndex], h[nextIdx]) { + return true + } + return false +} + +// isPartialPortionOfMinorTransition returns true if the entry at entryIndex is a partial update that is between completed +// updates that have transitioned the version from one minor version to another. +func isPartialPortionOfMinorTransition(entryIndex int, h []configv1.UpdateHistory, maxHistorySize int) bool { + if h[entryIndex].State == configv1.CompletedUpdate || entryIndex == 0 || entryIndex == maxHistorySize { + return false + } + prevIdx := findNextOlderCompleted(entryIndex, h) + if prevIdx == entryIndex { + return false + } + nextIdx := findNextNewerCompleted(entryIndex, h) + if nextIdx == entryIndex || sameMinorVersion(h[prevIdx], h[nextIdx]) { + return false + } + return true +} + +// isPartialWithinAZStream returns true if the entry at entryIndex is a partial update that is between completed +// updates that have transitioned the version from one z-stream version to another. +func isPartialWithinAZStream(entryIndex int, h []configv1.UpdateHistory, maxHistorySize int) bool { + if h[entryIndex].State == configv1.CompletedUpdate || entryIndex == 0 || entryIndex == maxHistorySize { + return false + } + prevIdx := findNextOlderCompleted(entryIndex, h) + if prevIdx == entryIndex { + return false + } + nextIdx := findNextNewerCompleted(entryIndex, h) + if nextIdx == entryIndex || sameZStreamVersion(h[prevIdx], h[nextIdx]) { + return false + } + return true +} + +// getTheMostRecentCompletedEntryIndex returns the index of the entry that is the most recently completed update as defined +// by its position in the slice. The slice is ordered from the latest to the earliest update. +func getTheMostRecentCompletedEntryIndex(h []configv1.UpdateHistory) int { + idx := -1 + for i, entry := range h { + if entry.State == configv1.CompletedUpdate { + idx = i + break + } + } + return idx +} + +// findNextOlderCompleted starts at entryIndex and returns the index of the entry that is the next older completed +// update as defined by its position in the slice. The slice is ordered from the latest to the earliest update. +func findNextOlderCompleted(entryIndex int, h []configv1.UpdateHistory) int { + idx := entryIndex + for i := entryIndex + 1; i < len(h); i++ { + if h[i].State == configv1.CompletedUpdate { + idx = i + break + } + } + return idx +} + +// findNextNewerCompleted starts at entryIndex and returns the index of the entry that is the next newer completed +// update as defined by its position in the slice. The slice is ordered from the latest to the earliest update. +func findNextNewerCompleted(entryIndex int, h []configv1.UpdateHistory) int { + idx := entryIndex + for i := entryIndex - 1; i >= 0; i-- { + if h[i].State == configv1.CompletedUpdate { + idx = i + break + } + } + return idx +} + +// sameMinorVersion returns true if e1 is the same minor version as e2. +func sameMinorVersion(e1 configv1.UpdateHistory, e2 configv1.UpdateHistory) bool { + return getEffectiveMinor(e1.Version) == getEffectiveMinor(e2.Version) +} + +// sameZStreamVersion returns true if e1 is the same z-stream version as e2. +func sameZStreamVersion(e1 configv1.UpdateHistory, e2 configv1.UpdateHistory) bool { + return getEffectiveMinor(e1.Version) == getEffectiveMinor(e2.Version) && + getEffectiveMicro(e1.Version) == getEffectiveMicro(e2.Version) +} + +// getEffectiveMinor attempts to parse the given version string as x.y. If it does not parse an empty string is returned +// otherwise the minor version y is returned. +func getEffectiveMinor(version string) string { + splits := strings.Split(version, ".") + if len(splits) < 2 { + return "" + } + return splits[1] +} + +// getEffectiveMicro attempts to parse the given version string as x.y.z. If it does not parse an empty string is returned +// otherwise the micro (z-stream) version z is returned. +func getEffectiveMicro(version string) string { + splits := strings.Split(version, ".") + if len(splits) < 3 { + return "" + } + return splits[2] +} diff --git a/pkg/cvo/status_history_test.go b/pkg/cvo/status_history_test.go new file mode 100644 index 0000000000..1a5d285b9a --- /dev/null +++ b/pkg/cvo/status_history_test.go @@ -0,0 +1,777 @@ +package cvo + +import ( + "reflect" + "testing" + + "k8s.io/apimachinery/pkg/util/diff" + "k8s.io/klog/v2" + + configv1 "github.com/openshift/api/config/v1" +) + +func Test_prune(t *testing.T) { + tests := []struct { + name string + history []configv1.UpdateHistory + maxHistory int + want []configv1.UpdateHistory + }{ + { + name: "partial update within a minor transition", + history: []configv1.UpdateHistory{ + {State: configv1.CompletedUpdate, Version: "4.6.3"}, + {State: configv1.CompletedUpdate, Version: "4.5.3"}, + {State: configv1.CompletedUpdate, Version: "4.4.3"}, + {State: configv1.CompletedUpdate, Version: "4.3.3"}, + {State: configv1.CompletedUpdate, Version: "4.3.2"}, + {State: configv1.CompletedUpdate, Version: "4.3.1"}, + {State: configv1.CompletedUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.1.4"}, + {State: configv1.PartialUpdate, Version: "4.1.3"}, + {State: configv1.PartialUpdate, Version: "4.1.2"}, + {State: configv1.CompletedUpdate, Version: "4.1.1"}, + }, + maxHistory: 10, + want: []configv1.UpdateHistory{ + {State: configv1.CompletedUpdate, Version: "4.6.3"}, + {State: configv1.CompletedUpdate, Version: "4.5.3"}, + {State: configv1.CompletedUpdate, Version: "4.4.3"}, + {State: configv1.CompletedUpdate, Version: "4.3.3"}, + {State: configv1.CompletedUpdate, Version: "4.3.2"}, + {State: configv1.CompletedUpdate, Version: "4.3.1"}, + {State: configv1.CompletedUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.1.4"}, + {State: configv1.PartialUpdate, Version: "4.1.3"}, + {State: configv1.CompletedUpdate, Version: "4.1.1"}, + }, + }, + { + name: "partial update within a z-stream transition", + history: []configv1.UpdateHistory{ + {State: configv1.CompletedUpdate, Version: "4.5.3"}, + {State: configv1.CompletedUpdate, Version: "4.4.3"}, + {State: configv1.CompletedUpdate, Version: "4.4.3"}, + {State: configv1.CompletedUpdate, Version: "4.4.2"}, + {State: configv1.CompletedUpdate, Version: "4.1.10"}, + {State: configv1.CompletedUpdate, Version: "4.1.9"}, + {State: configv1.CompletedUpdate, Version: "4.1.4"}, + {State: configv1.PartialUpdate, Version: "4.1.3"}, + {State: configv1.CompletedUpdate, Version: "4.1.2"}, + {State: configv1.PartialUpdate, Version: "4.0.1"}, + {State: configv1.CompletedUpdate, Version: "4.0.1"}, + }, + maxHistory: 10, + want: []configv1.UpdateHistory{ + {State: configv1.CompletedUpdate, Version: "4.5.3"}, + {State: configv1.CompletedUpdate, Version: "4.4.3"}, + {State: configv1.CompletedUpdate, Version: "4.4.3"}, + {State: configv1.CompletedUpdate, Version: "4.4.2"}, + {State: configv1.CompletedUpdate, Version: "4.1.10"}, + {State: configv1.CompletedUpdate, Version: "4.1.9"}, + {State: configv1.CompletedUpdate, Version: "4.1.4"}, + {State: configv1.CompletedUpdate, Version: "4.1.2"}, + {State: configv1.PartialUpdate, Version: "4.0.1"}, + {State: configv1.CompletedUpdate, Version: "4.0.1"}, + }, + }, + { + name: "prune oldest not in mostImportantWeight set", + history: []configv1.UpdateHistory{ + {State: configv1.CompletedUpdate, Version: "4.11.0"}, + {State: configv1.CompletedUpdate, Version: "4.10.0"}, + {State: configv1.CompletedUpdate, Version: "4.9.0"}, + {State: configv1.CompletedUpdate, Version: "4.8.0"}, + {State: configv1.CompletedUpdate, Version: "4.7.0"}, + {State: configv1.CompletedUpdate, Version: "4.6.0"}, + {State: configv1.CompletedUpdate, Version: "4.5.0"}, + {State: configv1.CompletedUpdate, Version: "4.4.0"}, + {State: configv1.CompletedUpdate, Version: "4.3.0"}, + {State: configv1.CompletedUpdate, Version: "4.2.0"}, + {State: configv1.CompletedUpdate, Version: "4.1.0"}, + }, + maxHistory: 10, + want: []configv1.UpdateHistory{ + {State: configv1.CompletedUpdate, Version: "4.11.0"}, + {State: configv1.CompletedUpdate, Version: "4.10.0"}, + {State: configv1.CompletedUpdate, Version: "4.9.0"}, + {State: configv1.CompletedUpdate, Version: "4.8.0"}, + {State: configv1.CompletedUpdate, Version: "4.7.0"}, + {State: configv1.CompletedUpdate, Version: "4.6.0"}, + {State: configv1.CompletedUpdate, Version: "4.5.0"}, + {State: configv1.CompletedUpdate, Version: "4.4.0"}, + {State: configv1.CompletedUpdate, Version: "4.3.0"}, + {State: configv1.CompletedUpdate, Version: "4.1.0"}, + }, + }, + { + name: "prune only partial not in mostImportantWeight set", + history: []configv1.UpdateHistory{ + {State: configv1.CompletedUpdate, Version: "4.11.0"}, + {State: configv1.PartialUpdate, Version: "4.10.0"}, + {State: configv1.CompletedUpdate, Version: "4.9.0"}, + {State: configv1.CompletedUpdate, Version: "4.8.0"}, + {State: configv1.CompletedUpdate, Version: "4.7.0"}, + {State: configv1.PartialUpdate, Version: "4.6.0"}, + {State: configv1.CompletedUpdate, Version: "4.5.0"}, + {State: configv1.CompletedUpdate, Version: "4.4.0"}, + {State: configv1.CompletedUpdate, Version: "4.3.0"}, + {State: configv1.CompletedUpdate, Version: "4.2.0"}, + {State: configv1.CompletedUpdate, Version: "4.1.0"}, + }, + maxHistory: 10, + want: []configv1.UpdateHistory{ + {State: configv1.CompletedUpdate, Version: "4.11.0"}, + {State: configv1.PartialUpdate, Version: "4.10.0"}, + {State: configv1.CompletedUpdate, Version: "4.9.0"}, + {State: configv1.CompletedUpdate, Version: "4.8.0"}, + {State: configv1.CompletedUpdate, Version: "4.7.0"}, + {State: configv1.CompletedUpdate, Version: "4.5.0"}, + {State: configv1.CompletedUpdate, Version: "4.4.0"}, + {State: configv1.CompletedUpdate, Version: "4.3.0"}, + {State: configv1.CompletedUpdate, Version: "4.2.0"}, + {State: configv1.CompletedUpdate, Version: "4.1.0"}, + }, + }, + { + name: "prune the z-stream partial over the minor partial", + history: []configv1.UpdateHistory{ + {State: configv1.CompletedUpdate, Version: "4.11.0"}, + {State: configv1.CompletedUpdate, Version: "4.10.0"}, + {State: configv1.CompletedUpdate, Version: "4.9.0"}, + {State: configv1.CompletedUpdate, Version: "4.8.0"}, + {State: configv1.CompletedUpdate, Version: "4.6.1"}, + {State: configv1.PartialUpdate, Version: "4.6.1"}, + {State: configv1.CompletedUpdate, Version: "4.6.0"}, + {State: configv1.CompletedUpdate, Version: "4.4.0"}, + {State: configv1.PartialUpdate, Version: "4.3.0"}, + {State: configv1.CompletedUpdate, Version: "4.2.0"}, + {State: configv1.CompletedUpdate, Version: "4.1.0"}, + }, + maxHistory: 10, + want: []configv1.UpdateHistory{ + {State: configv1.CompletedUpdate, Version: "4.11.0"}, + {State: configv1.CompletedUpdate, Version: "4.10.0"}, + {State: configv1.CompletedUpdate, Version: "4.9.0"}, + {State: configv1.CompletedUpdate, Version: "4.8.0"}, + {State: configv1.CompletedUpdate, Version: "4.6.1"}, + {State: configv1.CompletedUpdate, Version: "4.6.0"}, + {State: configv1.CompletedUpdate, Version: "4.4.0"}, + {State: configv1.PartialUpdate, Version: "4.3.0"}, + {State: configv1.CompletedUpdate, Version: "4.2.0"}, + {State: configv1.CompletedUpdate, Version: "4.1.0"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h := prune(tt.history, tt.maxHistory) + if klog.V(2).Enabled() { + dumpHistory(t, h) + } + if !reflect.DeepEqual(tt.want, h) { + t.Fatalf("%s", diff.ObjectReflectDiff(tt.want, h)) + } + }) + } +} + +func dumpHistory(t *testing.T, h []configv1.UpdateHistory) { + for i, entry := range h { + t.Logf("%d: %s\t%s", i, entry.State, entry.Version) + } +} + +func Test_isTheInitialEntry(t *testing.T) { + tests := []struct { + name string + history []configv1.UpdateHistory + entryIdx int + maxHistory int + want bool + }{ + { + name: "is the initial entry", + history: []configv1.UpdateHistory{ + {State: configv1.PartialUpdate, Version: "0.0.10"}, + {State: configv1.PartialUpdate, Version: "0.0.9"}, + {State: configv1.PartialUpdate, Version: "0.0.8"}, + {State: configv1.CompletedUpdate, Version: "0.0.7"}, + {State: configv1.PartialUpdate, Version: "0.0.6"}, + }, + entryIdx: 4, + maxHistory: 4, + want: true, + }, + { + name: "is not the initial entry", + history: []configv1.UpdateHistory{ + {State: configv1.PartialUpdate, Version: "0.0.10"}, + {State: configv1.PartialUpdate, Version: "0.0.9"}, + {State: configv1.PartialUpdate, Version: "0.0.8"}, + {State: configv1.CompletedUpdate, Version: "0.0.7"}, + {State: configv1.PartialUpdate, Version: "0.0.6"}, + }, + entryIdx: 3, + maxHistory: 4, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + isIt := isTheInitialEntry(tt.entryIdx, tt.maxHistory) + if !reflect.DeepEqual(tt.want, isIt) { + t.Fatalf("%s", diff.ObjectReflectDiff(tt.want, isIt)) + } + }) + } +} + +func Test_isAFinalEntry(t *testing.T) { + tests := []struct { + name string + history []configv1.UpdateHistory + entryIdx int + want bool + }{ + { + name: "is a final entry", + history: []configv1.UpdateHistory{ + {State: configv1.PartialUpdate, Version: "0.0.10"}, + {State: configv1.PartialUpdate, Version: "0.0.9"}, + {State: configv1.PartialUpdate, Version: "0.0.8"}, + {State: configv1.CompletedUpdate, Version: "0.0.7"}, + {State: configv1.PartialUpdate, Version: "0.0.6"}, + {State: configv1.PartialUpdate, Version: "0.0.5"}, + }, + entryIdx: 4, + want: true, + }, + { + name: "is not a final entry", + history: []configv1.UpdateHistory{ + {State: configv1.PartialUpdate, Version: "0.0.10"}, + {State: configv1.PartialUpdate, Version: "0.0.9"}, + {State: configv1.PartialUpdate, Version: "0.0.8"}, + {State: configv1.CompletedUpdate, Version: "0.0.7"}, + {State: configv1.PartialUpdate, Version: "0.0.6"}, + {State: configv1.PartialUpdate, Version: "0.0.5"}, + }, + entryIdx: 5, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + isIt := isAFinalEntry(tt.entryIdx) + if !reflect.DeepEqual(tt.want, isIt) { + t.Fatalf("%s", diff.ObjectReflectDiff(tt.want, isIt)) + } + }) + } +} + +func Test_isTheMostRecentCompletedEntry(t *testing.T) { + tests := []struct { + name string + history []configv1.UpdateHistory + entryIdx int + want bool + }{ + { + name: "is the most recent completed entry", + history: []configv1.UpdateHistory{ + {State: configv1.PartialUpdate, Version: "0.0.10"}, + {State: configv1.CompletedUpdate, Version: "0.0.9"}, + {State: configv1.PartialUpdate, Version: "0.0.8"}, + {State: configv1.CompletedUpdate, Version: "0.0.7"}, + {State: configv1.PartialUpdate, Version: "0.0.6"}, + {State: configv1.PartialUpdate, Version: "0.0.5"}, + }, + entryIdx: 1, + want: true, + }, + { + name: "is not the most recent completed entry", + history: []configv1.UpdateHistory{ + {State: configv1.PartialUpdate, Version: "0.0.10"}, + {State: configv1.CompletedUpdate, Version: "0.0.9"}, + {State: configv1.PartialUpdate, Version: "0.0.8"}, + {State: configv1.CompletedUpdate, Version: "0.0.7"}, + {State: configv1.PartialUpdate, Version: "0.0.6"}, + {State: configv1.PartialUpdate, Version: "0.0.5"}, + }, + entryIdx: 3, + want: false, + }, + { + name: "is not the most recent completed entry because partial", + history: []configv1.UpdateHistory{ + {State: configv1.PartialUpdate, Version: "0.0.10"}, + {State: configv1.CompletedUpdate, Version: "0.0.9"}, + {State: configv1.PartialUpdate, Version: "0.0.8"}, + {State: configv1.CompletedUpdate, Version: "0.0.7"}, + {State: configv1.PartialUpdate, Version: "0.0.6"}, + {State: configv1.PartialUpdate, Version: "0.0.5"}, + }, + entryIdx: 2, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mostRecentIdx := getTheMostRecentCompletedEntryIndex(tt.history) + isIt := isTheMostRecentCompletedEntry(tt.entryIdx, mostRecentIdx) + if !reflect.DeepEqual(tt.want, isIt) { + t.Fatalf("%s", diff.ObjectReflectDiff(tt.want, isIt)) + } + }) + } +} + +func Test_isTheFirstOrLastCompletedInAMinor(t *testing.T) { + tests := []struct { + name string + history []configv1.UpdateHistory + entryIdx int + maxHistory int + want bool + }{ + { + name: "is the first completed in a minor", + history: []configv1.UpdateHistory{ + {State: configv1.CompletedUpdate, Version: "4.3.3"}, + {State: configv1.CompletedUpdate, Version: "4.3.2"}, + {State: configv1.CompletedUpdate, Version: "4.3.1"}, + {State: configv1.CompletedUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.CompletedUpdate, Version: "4.1.1"}, + }, + entryIdx: 3, + maxHistory: 7, + want: true, + }, + { + name: "is the last completed in a minor", + history: []configv1.UpdateHistory{ + {State: configv1.PartialUpdate, Version: "4.3.3"}, + {State: configv1.CompletedUpdate, Version: "4.3.2"}, + {State: configv1.CompletedUpdate, Version: "4.3.1"}, + {State: configv1.CompletedUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.CompletedUpdate, Version: "4.1.1"}, + }, + entryIdx: 1, + maxHistory: 7, + want: true, + }, + { + name: "is the first completed in a minor because it is the first", + history: []configv1.UpdateHistory{ + {State: configv1.CompletedUpdate, Version: "4.3.3"}, + {State: configv1.CompletedUpdate, Version: "4.3.2"}, + {State: configv1.CompletedUpdate, Version: "4.3.1"}, + {State: configv1.CompletedUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.CompletedUpdate, Version: "4.1.1"}, + }, + entryIdx: 7, + maxHistory: 7, + want: true, + }, + { + name: "is the last completed in a minor because it is the last", + history: []configv1.UpdateHistory{ + {State: configv1.CompletedUpdate, Version: "4.3.3"}, + {State: configv1.CompletedUpdate, Version: "4.3.2"}, + {State: configv1.CompletedUpdate, Version: "4.3.1"}, + {State: configv1.CompletedUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.CompletedUpdate, Version: "4.1.1"}, + }, + entryIdx: 0, + maxHistory: 7, + want: true, + }, + { + name: "is not the first or the last completed in a minor, partial", + history: []configv1.UpdateHistory{ + {State: configv1.CompletedUpdate, Version: "4.3.3"}, + {State: configv1.CompletedUpdate, Version: "4.3.2"}, + {State: configv1.CompletedUpdate, Version: "4.3.1"}, + {State: configv1.CompletedUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.CompletedUpdate, Version: "4.1.1"}, + }, + entryIdx: 4, + maxHistory: 7, + want: false, + }, + { + name: "is not the first or the last completed in a minor", + history: []configv1.UpdateHistory{ + {State: configv1.CompletedUpdate, Version: "4.3.3"}, + {State: configv1.CompletedUpdate, Version: "4.3.2"}, + {State: configv1.PartialUpdate, Version: "4.4.1"}, + {State: configv1.CompletedUpdate, Version: "4.3.1"}, + {State: configv1.CompletedUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.CompletedUpdate, Version: "4.1.1"}, + }, + entryIdx: 1, + maxHistory: 8, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + isIt := isTheFirstOrLastCompletedInAMinor(tt.entryIdx, tt.history, tt.maxHistory) + if !reflect.DeepEqual(tt.want, isIt) { + t.Fatalf("%s", diff.ObjectReflectDiff(tt.want, isIt)) + } + }) + } +} + +func Test_isPartialPortionOfMinorTransition(t *testing.T) { + tests := []struct { + name string + history []configv1.UpdateHistory + entryIdx int + maxHistory int + want bool + }{ + { + name: "is the partial portion of a minor transition", + history: []configv1.UpdateHistory{ + {State: configv1.CompletedUpdate, Version: "4.3.3"}, + {State: configv1.CompletedUpdate, Version: "4.3.2"}, + {State: configv1.CompletedUpdate, Version: "4.3.1"}, + {State: configv1.CompletedUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.CompletedUpdate, Version: "4.1.1"}, + }, + entryIdx: 6, + maxHistory: 7, + want: true, + }, + { + name: "is the partial portion of a minor transition, diff target minor", + history: []configv1.UpdateHistory{ + {State: configv1.CompletedUpdate, Version: "4.3.3"}, + {State: configv1.CompletedUpdate, Version: "4.3.2"}, + {State: configv1.CompletedUpdate, Version: "4.3.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.CompletedUpdate, Version: "4.1.1"}, + }, + entryIdx: 3, + maxHistory: 5, + want: true, + }, + { + name: "is the partial portion of a minor transition, same source minor", + history: []configv1.UpdateHistory{ + {State: configv1.CompletedUpdate, Version: "4.3.3"}, + {State: configv1.CompletedUpdate, Version: "4.3.2"}, + {State: configv1.CompletedUpdate, Version: "4.3.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.CompletedUpdate, Version: "4.2.1"}, + }, + entryIdx: 3, + maxHistory: 5, + want: true, + }, + { + name: "is not the partial portion of a minor transition because it is the first", + history: []configv1.UpdateHistory{ + {State: configv1.CompletedUpdate, Version: "4.3.3"}, + {State: configv1.CompletedUpdate, Version: "4.3.2"}, + {State: configv1.CompletedUpdate, Version: "4.3.1"}, + {State: configv1.CompletedUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.1.1"}, + }, + entryIdx: 7, + maxHistory: 7, + want: false, + }, + { + name: "is not the partial portion of a minor transition because it is the last", + history: []configv1.UpdateHistory{ + {State: configv1.PartialUpdate, Version: "4.3.3"}, + {State: configv1.CompletedUpdate, Version: "4.3.2"}, + {State: configv1.CompletedUpdate, Version: "4.3.1"}, + {State: configv1.CompletedUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.1.1"}, + }, + entryIdx: 0, + maxHistory: 7, + want: false, + }, + { + name: "is not the partial portion of a minor transition because it is completed", + history: []configv1.UpdateHistory{ + {State: configv1.CompletedUpdate, Version: "4.3.3"}, + {State: configv1.CompletedUpdate, Version: "4.3.2"}, + {State: configv1.CompletedUpdate, Version: "4.3.1"}, + {State: configv1.CompletedUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.CompletedUpdate, Version: "4.1.1"}, + }, + entryIdx: 3, + maxHistory: 7, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + isIt := isPartialPortionOfMinorTransition(tt.entryIdx, tt.history, tt.maxHistory) + if !reflect.DeepEqual(tt.want, isIt) { + t.Fatalf("%s", diff.ObjectReflectDiff(tt.want, isIt)) + } + }) + } +} + +func Test_isPartialWithinAZStream(t *testing.T) { + tests := []struct { + name string + history []configv1.UpdateHistory + entryIdx int + maxHistory int + want bool + }{ + { + name: "is the partial portion of a z-stream transition", + history: []configv1.UpdateHistory{ + {State: configv1.CompletedUpdate, Version: "4.3.3"}, + {State: configv1.CompletedUpdate, Version: "4.3.2"}, + {State: configv1.CompletedUpdate, Version: "4.3.1"}, + {State: configv1.CompletedUpdate, Version: "4.2.2"}, + {State: configv1.PartialUpdate, Version: "4.2.2"}, + {State: configv1.PartialUpdate, Version: "4.2.2"}, + {State: configv1.PartialUpdate, Version: "4.2.2"}, + {State: configv1.CompletedUpdate, Version: "4.2.1"}, + }, + entryIdx: 6, + maxHistory: 7, + want: true, + }, + { + name: "is the partial portion of a z-stream transition, diff target micro", + history: []configv1.UpdateHistory{ + {State: configv1.CompletedUpdate, Version: "4.3.3"}, + {State: configv1.CompletedUpdate, Version: "4.3.2"}, + {State: configv1.CompletedUpdate, Version: "4.2.3"}, + {State: configv1.PartialUpdate, Version: "4.2.2"}, + {State: configv1.CompletedUpdate, Version: "4.2.1"}, + }, + entryIdx: 3, + maxHistory: 5, + want: true, + }, + { + name: "is the partial portion of a z-stream transition, same source micro", + history: []configv1.UpdateHistory{ + {State: configv1.CompletedUpdate, Version: "4.3.3"}, + {State: configv1.CompletedUpdate, Version: "4.3.2"}, + {State: configv1.CompletedUpdate, Version: "4.2.2"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.CompletedUpdate, Version: "4.2.1"}, + }, + entryIdx: 3, + maxHistory: 5, + want: true, + }, + { + name: "is not the partial portion of a z-stream transition because it is the first", + history: []configv1.UpdateHistory{ + {State: configv1.CompletedUpdate, Version: "4.3.3"}, + {State: configv1.CompletedUpdate, Version: "4.3.2"}, + {State: configv1.CompletedUpdate, Version: "4.3.1"}, + {State: configv1.CompletedUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.1.1"}, + }, + entryIdx: 7, + maxHistory: 7, + want: false, + }, + { + name: "is not the partial portion of a z-stream transition because it is the last", + history: []configv1.UpdateHistory{ + {State: configv1.PartialUpdate, Version: "4.3.3"}, + {State: configv1.CompletedUpdate, Version: "4.3.2"}, + {State: configv1.CompletedUpdate, Version: "4.3.1"}, + {State: configv1.CompletedUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.1.1"}, + }, + entryIdx: 0, + maxHistory: 7, + want: false, + }, + { + name: "is not the partial portion of a z-stream transition because it is completed", + history: []configv1.UpdateHistory{ + {State: configv1.CompletedUpdate, Version: "4.3.3"}, + {State: configv1.CompletedUpdate, Version: "4.3.2"}, + {State: configv1.CompletedUpdate, Version: "4.3.1"}, + {State: configv1.CompletedUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.PartialUpdate, Version: "4.2.1"}, + {State: configv1.CompletedUpdate, Version: "4.1.1"}, + }, + entryIdx: 3, + maxHistory: 7, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + isIt := isPartialWithinAZStream(tt.entryIdx, tt.history, tt.maxHistory) + if !reflect.DeepEqual(tt.want, isIt) { + t.Fatalf("%s", diff.ObjectReflectDiff(tt.want, isIt)) + } + }) + } +} + +func Test_sameMinorVersion(t *testing.T) { + tests := []struct { + name string + h1 configv1.UpdateHistory + h2 configv1.UpdateHistory + want bool + }{ + { + name: "is same minor version", + h1: configv1.UpdateHistory{Version: "4.3.3"}, + h2: configv1.UpdateHistory{Version: "4.3.2"}, + want: true, + }, + { + name: "is same minor version, no major", + h1: configv1.UpdateHistory{Version: "4.3.3"}, + h2: configv1.UpdateHistory{Version: ".3.2"}, + want: true, + }, + { + name: "is same minor version, no micro", + h1: configv1.UpdateHistory{Version: "4.3.3"}, + h2: configv1.UpdateHistory{Version: "4.3"}, + want: true, + }, + { + name: "is not same minor version", + h1: configv1.UpdateHistory{Version: "4.4.3"}, + h2: configv1.UpdateHistory{Version: "4.3.2"}, + want: false, + }, + { + name: "is not same minor version, empty", + h1: configv1.UpdateHistory{Version: "4.4.3"}, + h2: configv1.UpdateHistory{Version: ""}, + want: false, + }, + { + name: "is not same minor version, no dots", + h1: configv1.UpdateHistory{Version: "4.3.3"}, + h2: configv1.UpdateHistory{Version: "432"}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + isIt := sameMinorVersion(tt.h1, tt.h2) + if !reflect.DeepEqual(tt.want, isIt) { + t.Fatalf("%s", diff.ObjectReflectDiff(tt.want, isIt)) + } + }) + } +} +func Test_sameZStreamVersion(t *testing.T) { + tests := []struct { + name string + h1 configv1.UpdateHistory + h2 configv1.UpdateHistory + want bool + }{ + { + name: "is same z-stream version", + h1: configv1.UpdateHistory{Version: "4.3.3"}, + h2: configv1.UpdateHistory{Version: "4.3.3"}, + want: true, + }, + { + name: "is same z-stream version, no major", + h1: configv1.UpdateHistory{Version: "4.3.3"}, + h2: configv1.UpdateHistory{Version: ".3.3"}, + want: true, + }, + { + name: "is not same z-stream version", + h1: configv1.UpdateHistory{Version: "4.4.3"}, + h2: configv1.UpdateHistory{Version: "4.4.2"}, + want: false, + }, + { + name: "is not same z-stream version, no minor", + h1: configv1.UpdateHistory{Version: "4.3.3"}, + h2: configv1.UpdateHistory{Version: "4..3"}, + want: false, + }, + { + name: "is not same z-stream version, diff minor", + h1: configv1.UpdateHistory{Version: "4.4.3"}, + h2: configv1.UpdateHistory{Version: "4.3.3"}, + want: false, + }, + { + name: "is not same z-stream version, empty", + h1: configv1.UpdateHistory{Version: "4.4.3"}, + h2: configv1.UpdateHistory{Version: ""}, + want: false, + }, + { + name: "is not same z-stream version, no dots", + h1: configv1.UpdateHistory{Version: "4.3.3"}, + h2: configv1.UpdateHistory{Version: "432"}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + isIt := sameZStreamVersion(tt.h1, tt.h2) + if !reflect.DeepEqual(tt.want, isIt) { + t.Fatalf("%s", diff.ObjectReflectDiff(tt.want, isIt)) + } + }) + } +} diff --git a/pkg/cvo/status_test.go b/pkg/cvo/status_test.go index 9e3291acbc..f9985f34ad 100644 --- a/pkg/cvo/status_test.go +++ b/pkg/cvo/status_test.go @@ -5,7 +5,6 @@ import ( "fmt" "reflect" "testing" - "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/diff" @@ -62,153 +61,6 @@ func Test_mergeEqualVersions(t *testing.T) { } } -func Test_pruneStatusHistory(t *testing.T) { - obj := &configv1.ClusterVersion{ - Status: configv1.ClusterVersionStatus{ - History: []configv1.UpdateHistory{ - {State: configv1.PartialUpdate, Version: "0.0.10"}, - {State: configv1.PartialUpdate, Version: "0.0.9"}, - {State: configv1.PartialUpdate, Version: "0.0.8"}, - {State: configv1.CompletedUpdate, Version: "0.0.7"}, - {State: configv1.PartialUpdate, Version: "0.0.6"}, - }, - }, - } - tests := []struct { - name string - config *configv1.ClusterVersion - maxHistory int - want []configv1.UpdateHistory - }{ - { - name: "max history 2", - config: obj.DeepCopy(), - maxHistory: 2, - want: []configv1.UpdateHistory{ - {State: configv1.PartialUpdate, Version: "0.0.10"}, - {State: configv1.PartialUpdate, Version: "0.0.9"}, - }, - }, - { - name: "max history 3", - config: obj.DeepCopy(), - maxHistory: 3, - want: []configv1.UpdateHistory{ - {State: configv1.PartialUpdate, Version: "0.0.10"}, - {State: configv1.PartialUpdate, Version: "0.0.9"}, - {State: configv1.CompletedUpdate, Version: "0.0.7"}, - }, - }, - { - name: "max history 4", - config: obj.DeepCopy(), - maxHistory: 4, - want: []configv1.UpdateHistory{ - {State: configv1.PartialUpdate, Version: "0.0.10"}, - {State: configv1.PartialUpdate, Version: "0.0.9"}, - {State: configv1.PartialUpdate, Version: "0.0.8"}, - {State: configv1.CompletedUpdate, Version: "0.0.7"}, - }, - }, - { - name: "max history 5", - config: obj.DeepCopy(), - maxHistory: 5, - want: []configv1.UpdateHistory{ - {State: configv1.PartialUpdate, Version: "0.0.10"}, - {State: configv1.PartialUpdate, Version: "0.0.9"}, - {State: configv1.PartialUpdate, Version: "0.0.8"}, - {State: configv1.CompletedUpdate, Version: "0.0.7"}, - {State: configv1.PartialUpdate, Version: "0.0.6"}, - }, - }, - { - name: "max history 6", - config: obj.DeepCopy(), - maxHistory: 6, - want: []configv1.UpdateHistory{ - {State: configv1.PartialUpdate, Version: "0.0.10"}, - {State: configv1.PartialUpdate, Version: "0.0.9"}, - {State: configv1.PartialUpdate, Version: "0.0.8"}, - {State: configv1.CompletedUpdate, Version: "0.0.7"}, - {State: configv1.PartialUpdate, Version: "0.0.6"}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - config := tt.config.DeepCopy() - pruneStatusHistory(config, tt.maxHistory) - if !reflect.DeepEqual(tt.want, config.Status.History) { - t.Logf("%v", config.Status.History) - t.Fatalf("%s", diff.ObjectReflectDiff(tt.want, config.Status.History)) - } - }) - } -} - -func Test_mergeOperatorHistory(t *testing.T) { - obj := &configv1.ClusterVersion{} - var nowT time.Time - now := metav1.NewTime(nowT) - tests := []struct { - name string - config *configv1.ClusterVersion - first configv1.UpdateHistory - second configv1.UpdateHistory - want configv1.UpdateHistory - }{ - { - name: "drop partial", - config: obj.DeepCopy(), - first: configv1.UpdateHistory{State: configv1.PartialUpdate, CompletionTime: &now, Version: "0.0.7", Image: "test:0"}, - second: configv1.UpdateHistory{State: configv1.CompletedUpdate, CompletionTime: &now, Version: "0.0.6", Image: "test:1"}, - want: configv1.UpdateHistory{State: configv1.CompletedUpdate, CompletionTime: &now, Version: "0.0.6", Image: "test:1"}, - }, - { - name: "keep completed", - config: obj.DeepCopy(), - first: configv1.UpdateHistory{State: configv1.CompletedUpdate, CompletionTime: &now, Version: "0.0.7", Image: "test:0"}, - second: configv1.UpdateHistory{State: configv1.PartialUpdate, CompletionTime: &now, Version: "0.0.6", Image: "test:1"}, - want: configv1.UpdateHistory{State: configv1.CompletedUpdate, CompletionTime: &now, Version: "0.0.7", Image: "test:0"}, - }, - { - name: "neither completed", - config: obj.DeepCopy(), - first: configv1.UpdateHistory{State: configv1.PartialUpdate, CompletionTime: &now, Version: "0.0.7", Image: "test:0"}, - second: configv1.UpdateHistory{State: configv1.PartialUpdate, CompletionTime: &now, Version: "0.0.6", Image: "test:1"}, - want: configv1.UpdateHistory{State: configv1.PartialUpdate, CompletionTime: &now, Version: "0.0.6", Image: "test:1"}, - }, - { - name: "keep first completed", - config: obj.DeepCopy(), - first: configv1.UpdateHistory{State: configv1.CompletedUpdate, CompletionTime: &now, Version: "0.0.7", Image: "test:0"}, - second: configv1.UpdateHistory{State: configv1.CompletedUpdate, CompletionTime: &now, Version: "0.0.6", Image: "test:1"}, - want: configv1.UpdateHistory{State: configv1.CompletedUpdate, CompletionTime: &now, Version: "0.0.7", Image: "test:0"}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - for i := 0; i <= MaxHistory; i++ { - image := fmt.Sprintf("test:%d", i) - if i == 0 { - mergeOperatorHistory(tt.config, configv1.Release{Image: image, Version: tt.first.Version}, false, now, - (tt.first.State == configv1.CompletedUpdate)) - } else if i == 1 { - mergeOperatorHistory(tt.config, configv1.Release{Image: image, Version: tt.second.Version}, false, now, - (tt.second.State == configv1.CompletedUpdate)) - } else { - version := fmt.Sprintf("%d", i) - mergeOperatorHistory(tt.config, configv1.Release{Image: image, Version: version}, false, now, true) - } - } - if !reflect.DeepEqual(tt.want, tt.config.Status.History[MaxHistory-1]) { - t.Fatalf("%s", diff.ObjectReflectDiff(tt.want, tt.config.Status.History[MaxHistory-1])) - } - }) - } -} - func TestOperator_syncFailingStatus(t *testing.T) { ctx := context.Background() tests := []struct {