44 "context"
55 "fmt"
66 "reflect"
7+ "sort"
78 "strings"
89 "testing"
910 "time"
@@ -237,7 +238,10 @@ func testBoundTokenOperatorSecretDeletion(t testing.TB) {
237238 time .Sleep (kubeletTokenRefreshGracePeriod )
238239
239240 t .Log ("bouncing remaining crash-looping pods to get fresh SA tokens" )
240- err = bounceCrashLoopingPodsWithRetry (ctx , t , kubeClient , 25 * time .Minute )
241+ // Reduced from 25m to 20m to ensure we stay below monitor test thresholds.
242+ // With maxBouncesPerPod=50 and minTimeBetweenBounces=2m, max bounce time
243+ // is 100m, but most pods should recover much faster.
244+ err = bounceCrashLoopingPodsWithRetry (ctx , t , kubeClient , 20 * time .Minute )
241245 require .NoError (t , err )
242246
243247 t .Log ("waiting for all ClusterOperators to recover after key rotation rollouts" )
@@ -267,7 +271,10 @@ func testBoundTokenOperatorSecretDeletion(t testing.TB) {
267271 time .Sleep (kubeletTokenRefreshGracePeriod )
268272
269273 t .Log ("bouncing remaining crash-looping pods to get fresh SA tokens" )
270- err = bounceCrashLoopingPodsWithRetry (ctx , t , kubeClient , 25 * time .Minute )
274+ // Reduced from 25m to 20m to ensure we stay below monitor test thresholds.
275+ // With maxBouncesPerPod=25 and minTimeBetweenBounces=2m, max bounce time
276+ // is 30m, but most pods should recover much faster.
277+ err = bounceCrashLoopingPodsWithRetry (ctx , t , kubeClient , 20 * time .Minute )
271278 require .NoError (t , err )
272279
273280 t .Log ("waiting for all ClusterOperators to stabilize after cleanup rollout" )
@@ -352,13 +359,16 @@ func checkCertConfigMap(t testing.TB, kubeClient *clientcorev1.CoreV1Client, exp
352359// - Bounces in waves by namespace to reduce cluster load
353360// - Tracks recently bounced pods to avoid re-bouncing too quickly
354361// - Prioritizes operator pods over infrastructure pods
362+ // - Limits max bounces per pod to prevent excessive restarts in monitor tests
355363func bounceCrashLoopingPodsWithRetry (ctx context.Context , t testing.TB , kubeClient * clientcorev1.CoreV1Client , timeout time.Duration ) error {
356- const pollInterval = 45 * time .Second // Increased from 30s to give pods more time to recover
357- const minTimeBetweenBounces = 90 * time .Second // Don't re-bounce the same pod within 90 seconds
364+ const pollInterval = 45 * time .Second // Increased from 30s to give pods more time to recover
365+ const minTimeBetweenBounces = 120 * time .Second // Don't re-bounce the same pod within 2 minutes
366+ const maxBouncesPerPod = 50 // Maximum bounces per pod (monitor threshold is 20 restarts, but we give extra headroom)
358367
359368 var lastUnhealthyCount int
360369 totalBounced := 0
361- bouncedPods := make (map [string ]time.Time ) // Track when each pod was bounced
370+ bouncedPods := make (map [string ]time.Time ) // Track when each pod was last bounced
371+ bounceCount := make (map [string ]int ) // Track how many times each pod was bounced
362372
363373 t .Logf ("bouncing crash-looping pods until healthy (timeout: %v)" , timeout )
364374 err := wait .PollImmediate (pollInterval , timeout , func () (bool , error ) {
@@ -387,7 +397,7 @@ func bounceCrashLoopingPodsWithRetry(ctx context.Context, t testing.TB, kubeClie
387397
388398 // First pass: bounce high-priority operator namespaces
389399 for _ , nsName := range priorityNamespaces {
390- bounced , unhealthy , err := bouncePodsInNamespace (ctx , t , kubeClient , nsName , bouncedPods , minTimeBetweenBounces )
400+ bounced , unhealthy , err := bouncePodsInNamespace (ctx , t , kubeClient , nsName , bouncedPods , bounceCount , minTimeBetweenBounces , maxBouncesPerPod )
391401 if err != nil {
392402 // Skip this namespace but continue - transient API errors shouldn't fail the whole operation
393403 continue
@@ -413,7 +423,7 @@ func bounceCrashLoopingPodsWithRetry(ctx context.Context, t testing.TB, kubeClie
413423 continue
414424 }
415425
416- bounced , unhealthy , err := bouncePodsInNamespace (ctx , t , kubeClient , ns .Name , bouncedPods , minTimeBetweenBounces )
426+ bounced , unhealthy , err := bouncePodsInNamespace (ctx , t , kubeClient , ns .Name , bouncedPods , bounceCount , minTimeBetweenBounces , maxBouncesPerPod )
417427 if err != nil {
418428 // Skip this namespace but continue - transient API errors shouldn't fail the whole operation
419429 continue
@@ -445,7 +455,7 @@ func bounceCrashLoopingPodsWithRetry(ctx context.Context, t testing.TB, kubeClie
445455// bouncePodsInNamespace bounces crash-looping pods in a single namespace.
446456// Returns (number bounced, number unhealthy, error).
447457// If an error is returned, the counts are unreliable and should not be used.
448- func bouncePodsInNamespace (ctx context.Context , t testing.TB , kubeClient * clientcorev1.CoreV1Client , namespace string , bouncedPods map [string ]time.Time , minTimeBetweenBounces time.Duration ) (int , int , error ) {
458+ func bouncePodsInNamespace (ctx context.Context , t testing.TB , kubeClient * clientcorev1.CoreV1Client , namespace string , bouncedPods map [string ]time.Time , bounceCount map [ string ] int , minTimeBetweenBounces time.Duration , maxBouncesPerPod int ) (int , int , error ) {
449459 pods , err := kubeClient .Pods (namespace ).List (ctx , metav1.ListOptions {})
450460 if err != nil {
451461 t .Logf ("failed to list pods in %s: %v" , namespace , err )
@@ -455,6 +465,10 @@ func bouncePodsInNamespace(ctx context.Context, t testing.TB, kubeClient *client
455465 bounced := 0
456466 unhealthy := 0
457467
468+ // Sort pods by restart count (highest first) to prioritize pods
469+ // closest to the monitor test failure threshold (20 restarts)
470+ sortPodsByRestartCount (pods .Items )
471+
458472 for _ , pod := range pods .Items {
459473 if ! isPodCrashLooping (pod ) {
460474 continue
@@ -466,21 +480,29 @@ func bouncePodsInNamespace(ctx context.Context, t testing.TB, kubeClient *client
466480 continue
467481 }
468482
469- // Check if we bounced this pod recently
483+ // Check if we've exceeded max bounces for this pod
470484 podKey := namespace + "/" + pod .Name
485+ if count := bounceCount [podKey ]; count >= maxBouncesPerPod {
486+ t .Logf ("skipping pod %s/%s: already bounced %d times (max: %d)" ,
487+ namespace , pod .Name , count , maxBouncesPerPod )
488+ continue
489+ }
490+
491+ // Check if we bounced this pod recently
471492 if lastBounceTime , exists := bouncedPods [podKey ]; exists {
472493 if time .Since (lastBounceTime ) < minTimeBetweenBounces {
473494 continue // Don't re-bounce too quickly
474495 }
475496 }
476497
477- t .Logf ("bouncing crash-looping pod %s/%s (restarts: %d)" ,
478- namespace , pod .Name , getPodRestartCount (pod ))
498+ t .Logf ("bouncing crash-looping pod %s/%s (restarts: %d, bounce count: %d/%d )" ,
499+ namespace , pod .Name , getPodRestartCount (pod ), bounceCount [ podKey ], maxBouncesPerPod )
479500
480501 if err := kubeClient .Pods (namespace ).Delete (ctx , pod .Name , metav1.DeleteOptions {}); err != nil && ! apierrors .IsNotFound (err ) {
481502 t .Logf ("failed to delete pod %s/%s: %v" , namespace , pod .Name , err )
482503 } else {
483504 bouncedPods [podKey ] = time .Now ()
505+ bounceCount [podKey ]++
484506 bounced ++
485507 }
486508 }
@@ -536,10 +558,12 @@ func isPodEligibleForBounce(pod corev1.Pod) bool {
536558 return false
537559 }
538560
539- // Don't bounce pods with extremely high restart counts (>20)
540- // These likely have a different issue than SA token expiry
561+ // Don't bounce pods with extremely high restart counts (>40)
562+ // These likely have a different issue than SA token expiry.
563+ // We use 40 (2x the monitor threshold of 20) to ensure we can still
564+ // bounce pods that are approaching the monitor failure threshold.
541565 restartCount := getPodRestartCount (pod )
542- if restartCount > 20 {
566+ if restartCount > 40 {
543567 return false
544568 }
545569
@@ -555,6 +579,15 @@ func getPodRestartCount(pod corev1.Pod) int {
555579 return total
556580}
557581
582+ // sortPodsByRestartCount sorts pods by total restart count in descending order
583+ // (highest restart count first). This prioritizes bouncing pods closest to
584+ // the monitor test failure threshold.
585+ func sortPodsByRestartCount (pods []corev1.Pod ) {
586+ sort .Slice (pods , func (i , j int ) bool {
587+ return getPodRestartCount (pods [i ]) > getPodRestartCount (pods [j ])
588+ })
589+ }
590+
558591// waitForSATokenKeysOnAllNodes waits for the SA token signing key configmap
559592// to exist and be stable. This configmap is used by kubelet to issue SA tokens
560593// for application pods (it's not mounted in kube-apiserver itself).
0 commit comments