github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/cmd/sinker/main_test.go (about)

     1  /*
     2  Copyright 2016 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package main
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"flag"
    23  	"fmt"
    24  	"os"
    25  	"path/filepath"
    26  	"reflect"
    27  	"strconv"
    28  	"testing"
    29  	"time"
    30  
    31  	"github.com/google/go-cmp/cmp"
    32  	"github.com/sirupsen/logrus"
    33  	corev1api "k8s.io/api/core/v1"
    34  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    35  	"k8s.io/apimachinery/pkg/runtime"
    36  	"k8s.io/apimachinery/pkg/types"
    37  	"k8s.io/apimachinery/pkg/util/sets"
    38  	ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
    39  	fakectrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
    40  
    41  	prowv1 "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    42  	"sigs.k8s.io/prow/pkg/config"
    43  	"sigs.k8s.io/prow/pkg/flagutil"
    44  	configflagutil "sigs.k8s.io/prow/pkg/flagutil/config"
    45  	"sigs.k8s.io/prow/pkg/kube"
    46  )
    47  
    48  const (
    49  	maxProwJobAge    = 2 * 24 * time.Hour
    50  	maxPodAge        = 12 * time.Hour
    51  	terminatedPodTTL = 30 * time.Minute // must be less than maxPodAge
    52  )
    53  
    54  func newDefaultFakeSinkerConfig() config.Sinker {
    55  	return config.Sinker{
    56  		MaxProwJobAge:    &metav1.Duration{Duration: maxProwJobAge},
    57  		MaxPodAge:        &metav1.Duration{Duration: maxPodAge},
    58  		TerminatedPodTTL: &metav1.Duration{Duration: terminatedPodTTL},
    59  	}
    60  }
    61  
    62  type fca struct {
    63  	c *config.Config
    64  }
    65  
    66  func newFakeConfigAgent(s config.Sinker) *fca {
    67  	return &fca{
    68  		c: &config.Config{
    69  			ProwConfig: config.ProwConfig{
    70  				ProwJobNamespace: "ns",
    71  				PodNamespace:     "ns",
    72  				Sinker:           s,
    73  			},
    74  			JobConfig: config.JobConfig{
    75  				Periodics: []config.Periodic{
    76  					{JobBase: config.JobBase{Name: "retester"}},
    77  				},
    78  			},
    79  		},
    80  	}
    81  
    82  }
    83  
    84  func (f *fca) Config() *config.Config {
    85  	return f.c
    86  }
    87  
    88  func startTime(s time.Time) *metav1.Time {
    89  	start := metav1.NewTime(s)
    90  	return &start
    91  }
    92  
    93  type unreachableCluster struct{ ctrlruntimeclient.Client }
    94  
    95  func (unreachableCluster) Delete(_ context.Context, obj ctrlruntimeclient.Object, opts ...ctrlruntimeclient.DeleteOption) error {
    96  	return fmt.Errorf("I can't hear you.")
    97  }
    98  
    99  func (unreachableCluster) List(_ context.Context, _ ctrlruntimeclient.ObjectList, opts ...ctrlruntimeclient.ListOption) error {
   100  	return fmt.Errorf("I can't hear you.")
   101  }
   102  
   103  func (unreachableCluster) Patch(_ context.Context, _ ctrlruntimeclient.Object, _ ctrlruntimeclient.Patch, _ ...ctrlruntimeclient.PatchOption) error {
   104  	return errors.New("I can't hear you.")
   105  }
   106  
   107  func TestClean(t *testing.T) {
   108  
   109  	pods := []runtime.Object{
   110  		&corev1api.Pod{
   111  			ObjectMeta: metav1.ObjectMeta{
   112  				Name:      "job-running-pod-failed",
   113  				Namespace: "ns",
   114  				Labels: map[string]string{
   115  					kube.CreatedByProw:  "true",
   116  					kube.ProwJobIDLabel: "job-running",
   117  				},
   118  			},
   119  			Status: corev1api.PodStatus{
   120  				Phase:     corev1api.PodFailed,
   121  				StartTime: startTime(time.Now().Add(-maxPodAge).Add(-time.Second)),
   122  			},
   123  		},
   124  		&corev1api.Pod{
   125  			ObjectMeta: metav1.ObjectMeta{
   126  				Name:      "job-running-pod-succeeded",
   127  				Namespace: "ns",
   128  				Labels: map[string]string{
   129  					kube.CreatedByProw:  "true",
   130  					kube.ProwJobIDLabel: "job-running",
   131  				},
   132  			},
   133  			Status: corev1api.PodStatus{
   134  				Phase:     corev1api.PodSucceeded,
   135  				StartTime: startTime(time.Now().Add(-maxPodAge).Add(-time.Second)),
   136  			},
   137  		},
   138  		&corev1api.Pod{
   139  			ObjectMeta: metav1.ObjectMeta{
   140  				Name:      "job-complete-pod-failed",
   141  				Namespace: "ns",
   142  				Labels: map[string]string{
   143  					kube.CreatedByProw:  "true",
   144  					kube.ProwJobIDLabel: "job-complete",
   145  				},
   146  			},
   147  			Status: corev1api.PodStatus{
   148  				Phase:     corev1api.PodFailed,
   149  				StartTime: startTime(time.Now().Add(-maxPodAge).Add(-time.Second)),
   150  			},
   151  		},
   152  		&corev1api.Pod{
   153  			ObjectMeta: metav1.ObjectMeta{
   154  				Name:      "job-complete-pod-succeeded",
   155  				Namespace: "ns",
   156  				Labels: map[string]string{
   157  					kube.CreatedByProw:  "true",
   158  					kube.ProwJobIDLabel: "job-complete",
   159  				},
   160  			},
   161  			Status: corev1api.PodStatus{
   162  				Phase:     corev1api.PodSucceeded,
   163  				StartTime: startTime(time.Now().Add(-maxPodAge).Add(-time.Second)),
   164  			},
   165  		},
   166  		&corev1api.Pod{
   167  			ObjectMeta: metav1.ObjectMeta{
   168  				Name:      "job-complete-pod-pending",
   169  				Namespace: "ns",
   170  				Labels: map[string]string{
   171  					kube.CreatedByProw:  "true",
   172  					kube.ProwJobIDLabel: "job-complete",
   173  				},
   174  			},
   175  			Status: corev1api.PodStatus{
   176  				Phase:     corev1api.PodPending,
   177  				StartTime: startTime(time.Now().Add(-maxPodAge).Add(-time.Second)),
   178  			},
   179  		},
   180  		&corev1api.Pod{
   181  			ObjectMeta: metav1.ObjectMeta{
   182  				Name:      "job-unknown-pod-pending",
   183  				Namespace: "ns",
   184  				Labels: map[string]string{
   185  					kube.CreatedByProw:  "true",
   186  					kube.ProwJobIDLabel: "job-unknown",
   187  				},
   188  			},
   189  			Status: corev1api.PodStatus{
   190  				Phase:     corev1api.PodPending,
   191  				StartTime: startTime(time.Now().Add(-maxPodAge).Add(-time.Second)),
   192  			},
   193  		},
   194  		&corev1api.Pod{
   195  			ObjectMeta: metav1.ObjectMeta{
   196  				Name:      "job-unknown-pod-failed",
   197  				Namespace: "ns",
   198  				Labels: map[string]string{
   199  					kube.CreatedByProw:  "true",
   200  					kube.ProwJobIDLabel: "job-unknown",
   201  				},
   202  			},
   203  			Status: corev1api.PodStatus{
   204  				Phase:     corev1api.PodFailed,
   205  				StartTime: startTime(time.Now().Add(-maxPodAge).Add(-time.Second)),
   206  			},
   207  		},
   208  		&corev1api.Pod{
   209  			ObjectMeta: metav1.ObjectMeta{
   210  				Name:      "job-unknown-pod-succeeded",
   211  				Namespace: "ns",
   212  				Labels: map[string]string{
   213  					kube.CreatedByProw:  "true",
   214  					kube.ProwJobIDLabel: "job-unknown",
   215  				},
   216  			},
   217  			Status: corev1api.PodStatus{
   218  				Phase:     corev1api.PodSucceeded,
   219  				StartTime: startTime(time.Now().Add(-maxPodAge).Add(-time.Second)),
   220  			},
   221  		},
   222  		&corev1api.Pod{
   223  			ObjectMeta: metav1.ObjectMeta{
   224  				Name:      "old-failed",
   225  				Namespace: "ns",
   226  				Labels: map[string]string{
   227  					kube.CreatedByProw: "true",
   228  				},
   229  			},
   230  			Status: corev1api.PodStatus{
   231  				Phase:     corev1api.PodFailed,
   232  				StartTime: startTime(time.Now().Add(-maxPodAge).Add(-time.Second)),
   233  			},
   234  		},
   235  		&corev1api.Pod{
   236  			ObjectMeta: metav1.ObjectMeta{
   237  				Name:      "old-succeeded",
   238  				Namespace: "ns",
   239  				Labels: map[string]string{
   240  					kube.CreatedByProw: "true",
   241  				},
   242  			},
   243  			Status: corev1api.PodStatus{
   244  				Phase:     corev1api.PodSucceeded,
   245  				StartTime: startTime(time.Now().Add(-maxPodAge).Add(-time.Second)),
   246  			},
   247  		},
   248  		&corev1api.Pod{
   249  			ObjectMeta: metav1.ObjectMeta{
   250  				Name:      "old-just-complete",
   251  				Namespace: "ns",
   252  				Labels: map[string]string{
   253  					kube.CreatedByProw: "true",
   254  				},
   255  			},
   256  			Status: corev1api.PodStatus{
   257  				Phase:     corev1api.PodSucceeded,
   258  				StartTime: startTime(time.Now().Add(-maxPodAge).Add(-time.Second)),
   259  			},
   260  		},
   261  		&corev1api.Pod{
   262  			ObjectMeta: metav1.ObjectMeta{
   263  				Name:      "old-pending",
   264  				Namespace: "ns",
   265  				Labels: map[string]string{
   266  					kube.CreatedByProw: "true",
   267  				},
   268  			},
   269  			Status: corev1api.PodStatus{
   270  				Phase:     corev1api.PodPending,
   271  				StartTime: startTime(time.Now().Add(-maxPodAge).Add(-time.Second)),
   272  			},
   273  		},
   274  		&corev1api.Pod{
   275  			ObjectMeta: metav1.ObjectMeta{
   276  				Name:      "old-pending-abort",
   277  				Namespace: "ns",
   278  				Labels: map[string]string{
   279  					kube.CreatedByProw: "true",
   280  				},
   281  			},
   282  			Status: corev1api.PodStatus{
   283  				Phase:     corev1api.PodPending,
   284  				StartTime: startTime(time.Now().Add(-maxPodAge).Add(-time.Second)),
   285  			},
   286  		},
   287  		&corev1api.Pod{
   288  			ObjectMeta: metav1.ObjectMeta{
   289  				Name:      "new-failed",
   290  				Namespace: "ns",
   291  				Labels: map[string]string{
   292  					kube.CreatedByProw: "true",
   293  				},
   294  			},
   295  			Status: corev1api.PodStatus{
   296  				Phase:     corev1api.PodFailed,
   297  				StartTime: startTime(time.Now().Add(-10 * time.Second)),
   298  			},
   299  		},
   300  		&corev1api.Pod{
   301  			ObjectMeta: metav1.ObjectMeta{
   302  				Name:      "new-running-no-pj",
   303  				Namespace: "ns",
   304  				Labels: map[string]string{
   305  					kube.CreatedByProw: "true",
   306  				},
   307  			},
   308  			Status: corev1api.PodStatus{
   309  				Phase:     corev1api.PodRunning,
   310  				StartTime: startTime(time.Now().Add(-10 * time.Second)),
   311  			},
   312  		},
   313  		&corev1api.Pod{
   314  			ObjectMeta: metav1.ObjectMeta{
   315  				Name:      "old-running",
   316  				Namespace: "ns",
   317  				Labels: map[string]string{
   318  					kube.CreatedByProw: "true",
   319  				},
   320  			},
   321  			Status: corev1api.PodStatus{
   322  				Phase:     corev1api.PodRunning,
   323  				StartTime: startTime(time.Now().Add(-maxPodAge).Add(-time.Second)),
   324  			},
   325  		},
   326  		&corev1api.Pod{
   327  			ObjectMeta: metav1.ObjectMeta{
   328  				Name:      "unrelated-failed",
   329  				Namespace: "ns",
   330  				Labels: map[string]string{
   331  					kube.CreatedByProw: "not really",
   332  				},
   333  			},
   334  			Status: corev1api.PodStatus{
   335  				Phase:     corev1api.PodFailed,
   336  				StartTime: startTime(time.Now().Add(-maxPodAge).Add(-time.Second)),
   337  			},
   338  		},
   339  		&corev1api.Pod{
   340  			ObjectMeta: metav1.ObjectMeta{
   341  				Name:      "unrelated-complete",
   342  				Namespace: "ns",
   343  			},
   344  			Status: corev1api.PodStatus{
   345  				Phase:     corev1api.PodSucceeded,
   346  				StartTime: startTime(time.Now().Add(-maxPodAge).Add(-time.Second)),
   347  			},
   348  		},
   349  		&corev1api.Pod{
   350  			ObjectMeta: metav1.ObjectMeta{
   351  				Name:      "ttl-expired",
   352  				Namespace: "ns",
   353  				Labels: map[string]string{
   354  					kube.CreatedByProw: "true",
   355  				},
   356  			},
   357  			Status: corev1api.PodStatus{
   358  				Phase:     corev1api.PodFailed,
   359  				StartTime: startTime(time.Now().Add(-terminatedPodTTL * 2)),
   360  				ContainerStatuses: []corev1api.ContainerStatus{
   361  					{
   362  						State: corev1api.ContainerState{
   363  							Terminated: &corev1api.ContainerStateTerminated{
   364  								FinishedAt: metav1.Time{Time: time.Now().Add(-terminatedPodTTL).Add(-time.Second)},
   365  							},
   366  						},
   367  					},
   368  				},
   369  			},
   370  		},
   371  		&corev1api.Pod{
   372  			ObjectMeta: metav1.ObjectMeta{
   373  				Name:      "ttl-not-expired",
   374  				Namespace: "ns",
   375  				Labels: map[string]string{
   376  					kube.CreatedByProw: "true",
   377  				},
   378  			},
   379  			Status: corev1api.PodStatus{
   380  				Phase:     corev1api.PodFailed,
   381  				StartTime: startTime(time.Now().Add(-terminatedPodTTL * 2)),
   382  				ContainerStatuses: []corev1api.ContainerStatus{
   383  					{
   384  						State: corev1api.ContainerState{
   385  							Terminated: &corev1api.ContainerStateTerminated{
   386  								FinishedAt: metav1.Time{Time: time.Now().Add(-terminatedPodTTL).Add(-time.Second)},
   387  							},
   388  						},
   389  					},
   390  					{
   391  						State: corev1api.ContainerState{
   392  							Terminated: &corev1api.ContainerStateTerminated{
   393  								FinishedAt: metav1.Time{Time: time.Now().Add(-time.Second)},
   394  							},
   395  						},
   396  					},
   397  				},
   398  			},
   399  		},
   400  		&corev1api.Pod{
   401  			ObjectMeta: metav1.ObjectMeta{
   402  				Name:      "completed-prowjob-ttl-expired-while-pod-still-pending",
   403  				Namespace: "ns",
   404  				Labels: map[string]string{
   405  					kube.CreatedByProw: "true",
   406  				},
   407  			},
   408  			Status: corev1api.PodStatus{
   409  				Phase:     corev1api.PodPending,
   410  				StartTime: startTime(time.Now().Add(-terminatedPodTTL * 2)),
   411  				ContainerStatuses: []corev1api.ContainerStatus{
   412  					{
   413  						State: corev1api.ContainerState{
   414  							Waiting: &corev1api.ContainerStateWaiting{
   415  								Reason: "ImgPullBackoff",
   416  							},
   417  						},
   418  					},
   419  				},
   420  			},
   421  		},
   422  		&corev1api.Pod{
   423  			ObjectMeta: metav1.ObjectMeta{
   424  				Name:       "completed-and-reported-prowjob-pod-still-has-kubernetes-finalizer",
   425  				Namespace:  "ns",
   426  				Finalizers: []string{"prow.x-k8s.io/gcsk8sreporter"},
   427  				Labels: map[string]string{
   428  					kube.CreatedByProw: "true",
   429  				},
   430  			},
   431  			Status: corev1api.PodStatus{
   432  				Phase:     corev1api.PodPending,
   433  				StartTime: startTime(time.Now().Add(-terminatedPodTTL * 2)),
   434  			},
   435  		},
   436  		&corev1api.Pod{
   437  			ObjectMeta: metav1.ObjectMeta{
   438  				Name:       "completed-pod-without-prowjob-that-still-has-finalizer",
   439  				Namespace:  "ns",
   440  				Finalizers: []string{"prow.x-k8s.io/gcsk8sreporter"},
   441  				Labels: map[string]string{
   442  					kube.CreatedByProw: "true",
   443  				},
   444  			},
   445  			Status: corev1api.PodStatus{
   446  				Phase:     corev1api.PodPending,
   447  				StartTime: startTime(time.Now().Add(-terminatedPodTTL * 2)),
   448  			},
   449  		},
   450  		&corev1api.Pod{
   451  			ObjectMeta: metav1.ObjectMeta{
   452  				Name:      "very-young-orphaned-pod-is-kept-to-account-for-cache-staleness",
   453  				Namespace: "ns",
   454  				Labels: map[string]string{
   455  					kube.CreatedByProw: "true",
   456  				},
   457  				CreationTimestamp: metav1.Now(),
   458  			},
   459  		},
   460  		&corev1api.Pod{
   461  			ObjectMeta: metav1.ObjectMeta{
   462  				// The corresponding prowjob will only show up in a GET and not in a list requests. We do this to make
   463  				// sure that the orphan check does another get on the prowjob before declaring a pod orphaned rather
   464  				// than relying on the possibly outdated list created in the very beginning of the sync.
   465  				Name:      "get-only-prowjob",
   466  				Namespace: "ns",
   467  				Labels: map[string]string{
   468  					kube.CreatedByProw: "true",
   469  				},
   470  			},
   471  		},
   472  	}
   473  	deletedPods := sets.New[string](
   474  		"job-complete-pod-failed",
   475  		"job-complete-pod-pending",
   476  		"job-complete-pod-succeeded",
   477  		"job-unknown-pod-failed",
   478  		"job-unknown-pod-pending",
   479  		"job-unknown-pod-succeeded",
   480  		"new-running-no-pj",
   481  		"old-failed",
   482  		"old-succeeded",
   483  		"old-pending-abort",
   484  		"old-running",
   485  		"ttl-expired",
   486  		"completed-prowjob-ttl-expired-while-pod-still-pending",
   487  		"completed-and-reported-prowjob-pod-still-has-kubernetes-finalizer",
   488  		"completed-pod-without-prowjob-that-still-has-finalizer",
   489  	)
   490  	setComplete := func(d time.Duration) *metav1.Time {
   491  		completed := metav1.NewTime(time.Now().Add(d))
   492  		return &completed
   493  	}
   494  	prowJobs := []runtime.Object{
   495  		&prowv1.ProwJob{
   496  			ObjectMeta: metav1.ObjectMeta{
   497  				Name:      "job-complete",
   498  				Namespace: "ns",
   499  			},
   500  			Status: prowv1.ProwJobStatus{
   501  				StartTime:      metav1.NewTime(time.Now().Add(-maxProwJobAge).Add(-time.Second)),
   502  				CompletionTime: setComplete(-time.Second),
   503  			},
   504  		},
   505  		&prowv1.ProwJob{
   506  			ObjectMeta: metav1.ObjectMeta{
   507  				Name:      "job-running",
   508  				Namespace: "ns",
   509  			},
   510  			Status: prowv1.ProwJobStatus{
   511  				StartTime: metav1.NewTime(time.Now().Add(-maxProwJobAge).Add(-time.Second)),
   512  			},
   513  		},
   514  		&prowv1.ProwJob{
   515  			ObjectMeta: metav1.ObjectMeta{
   516  				Name:      "old-failed",
   517  				Namespace: "ns",
   518  			},
   519  			Status: prowv1.ProwJobStatus{
   520  				StartTime:      metav1.NewTime(time.Now().Add(-maxProwJobAge).Add(-time.Second)),
   521  				CompletionTime: setComplete(-time.Second),
   522  			},
   523  		},
   524  		&prowv1.ProwJob{
   525  			ObjectMeta: metav1.ObjectMeta{
   526  				Name:      "old-succeeded",
   527  				Namespace: "ns",
   528  			},
   529  			Status: prowv1.ProwJobStatus{
   530  				StartTime:      metav1.NewTime(time.Now().Add(-maxProwJobAge).Add(-time.Second)),
   531  				CompletionTime: setComplete(-time.Second),
   532  			},
   533  		},
   534  		&prowv1.ProwJob{
   535  			ObjectMeta: metav1.ObjectMeta{
   536  				Name:      "old-just-complete",
   537  				Namespace: "ns",
   538  			},
   539  			Status: prowv1.ProwJobStatus{
   540  				StartTime: metav1.NewTime(time.Now().Add(-maxProwJobAge).Add(-time.Second)),
   541  			},
   542  		},
   543  		&prowv1.ProwJob{
   544  			ObjectMeta: metav1.ObjectMeta{
   545  				Name:      "old-complete",
   546  				Namespace: "ns",
   547  			},
   548  			Status: prowv1.ProwJobStatus{
   549  				StartTime:      metav1.NewTime(time.Now().Add(-maxProwJobAge).Add(-time.Second)),
   550  				CompletionTime: setComplete(-time.Second),
   551  			},
   552  		},
   553  		&prowv1.ProwJob{
   554  			ObjectMeta: metav1.ObjectMeta{
   555  				Name:      "old-incomplete",
   556  				Namespace: "ns",
   557  			},
   558  			Status: prowv1.ProwJobStatus{
   559  				StartTime: metav1.NewTime(time.Now().Add(-maxProwJobAge).Add(-time.Second)),
   560  			},
   561  		},
   562  		&prowv1.ProwJob{
   563  			ObjectMeta: metav1.ObjectMeta{
   564  				Name:      "old-pending",
   565  				Namespace: "ns",
   566  			},
   567  			Status: prowv1.ProwJobStatus{
   568  				StartTime: metav1.NewTime(time.Now().Add(-maxProwJobAge).Add(-time.Second)),
   569  			},
   570  		},
   571  		&prowv1.ProwJob{
   572  			ObjectMeta: metav1.ObjectMeta{
   573  				Name:      "old-pending-abort",
   574  				Namespace: "ns",
   575  			},
   576  			Status: prowv1.ProwJobStatus{
   577  				StartTime:      metav1.NewTime(time.Now().Add(-maxProwJobAge).Add(-time.Second)),
   578  				CompletionTime: setComplete(-time.Second),
   579  			},
   580  		},
   581  		&prowv1.ProwJob{
   582  			ObjectMeta: metav1.ObjectMeta{
   583  				Name:      "new",
   584  				Namespace: "ns",
   585  			},
   586  			Status: prowv1.ProwJobStatus{
   587  				StartTime: metav1.NewTime(time.Now().Add(-time.Second)),
   588  			},
   589  		},
   590  		&prowv1.ProwJob{
   591  			ObjectMeta: metav1.ObjectMeta{
   592  				Name:      "newer-periodic",
   593  				Namespace: "ns",
   594  			},
   595  			Spec: prowv1.ProwJobSpec{
   596  				Type: prowv1.PeriodicJob,
   597  				Job:  "retester",
   598  			},
   599  			Status: prowv1.ProwJobStatus{
   600  				StartTime:      metav1.NewTime(time.Now().Add(-maxProwJobAge).Add(-time.Second)),
   601  				CompletionTime: setComplete(-time.Second),
   602  			},
   603  		},
   604  		&prowv1.ProwJob{
   605  			ObjectMeta: metav1.ObjectMeta{
   606  				Name:      "new-failed",
   607  				Namespace: "ns",
   608  			},
   609  			Status: prowv1.ProwJobStatus{
   610  				StartTime: metav1.NewTime(time.Now().Add(-time.Minute)),
   611  			},
   612  		},
   613  		&prowv1.ProwJob{
   614  			ObjectMeta: metav1.ObjectMeta{
   615  				Name:      "older-periodic",
   616  				Namespace: "ns",
   617  			},
   618  			Spec: prowv1.ProwJobSpec{
   619  				Type: prowv1.PeriodicJob,
   620  				Job:  "retester",
   621  			},
   622  			Status: prowv1.ProwJobStatus{
   623  				StartTime:      metav1.NewTime(time.Now().Add(-maxProwJobAge).Add(-time.Minute)),
   624  				CompletionTime: setComplete(-time.Minute),
   625  			},
   626  		},
   627  		&prowv1.ProwJob{
   628  			ObjectMeta: metav1.ObjectMeta{
   629  				Name:      "oldest-periodic",
   630  				Namespace: "ns",
   631  			},
   632  			Spec: prowv1.ProwJobSpec{
   633  				Type: prowv1.PeriodicJob,
   634  				Job:  "retester",
   635  			},
   636  			Status: prowv1.ProwJobStatus{
   637  				StartTime:      metav1.NewTime(time.Now().Add(-maxProwJobAge).Add(-time.Hour)),
   638  				CompletionTime: setComplete(-time.Hour),
   639  			},
   640  		},
   641  		&prowv1.ProwJob{
   642  			ObjectMeta: metav1.ObjectMeta{
   643  				Name:      "old-failed-trusted",
   644  				Namespace: "ns",
   645  			},
   646  			Status: prowv1.ProwJobStatus{
   647  				StartTime:      metav1.NewTime(time.Now().Add(-maxProwJobAge).Add(-time.Second)),
   648  				CompletionTime: setComplete(-time.Second),
   649  			},
   650  		},
   651  		&prowv1.ProwJob{
   652  			ObjectMeta: metav1.ObjectMeta{
   653  				Name:      "ttl-expired",
   654  				Namespace: "ns",
   655  			},
   656  			Status: prowv1.ProwJobStatus{
   657  				StartTime:      metav1.NewTime(time.Now().Add(-terminatedPodTTL * 2)),
   658  				CompletionTime: setComplete(-terminatedPodTTL - time.Second),
   659  			},
   660  		},
   661  		&prowv1.ProwJob{
   662  			ObjectMeta: metav1.ObjectMeta{
   663  				Name:      "ttl-not-expired",
   664  				Namespace: "ns",
   665  			},
   666  			Status: prowv1.ProwJobStatus{
   667  				StartTime:      metav1.NewTime(time.Now().Add(-terminatedPodTTL * 2)),
   668  				CompletionTime: setComplete(-time.Second),
   669  			},
   670  		},
   671  		&prowv1.ProwJob{
   672  			ObjectMeta: metav1.ObjectMeta{
   673  				Name:      "completed-prowjob-ttl-expired-while-pod-still-pending",
   674  				Namespace: "ns",
   675  			},
   676  			Status: prowv1.ProwJobStatus{
   677  				StartTime:      metav1.NewTime(time.Now().Add(-terminatedPodTTL * 2)),
   678  				CompletionTime: setComplete(-terminatedPodTTL - time.Second),
   679  			},
   680  		},
   681  		&prowv1.ProwJob{
   682  			ObjectMeta: metav1.ObjectMeta{
   683  				Name: "completed-and-reported-prowjob-pod-still-has-kubernetes-finalizer",
   684  			},
   685  			Status: prowv1.ProwJobStatus{
   686  				StartTime:        metav1.NewTime(time.Now().Add(-terminatedPodTTL * 2)),
   687  				CompletionTime:   setComplete(-terminatedPodTTL - time.Second),
   688  				PrevReportStates: map[string]prowv1.ProwJobState{"gcsk8sreporter": prowv1.AbortedState},
   689  			},
   690  		},
   691  	}
   692  
   693  	deletedProwJobs := sets.New[string](
   694  		"job-complete",
   695  		"old-failed",
   696  		"old-succeeded",
   697  		"old-complete",
   698  		"old-pending-abort",
   699  		"older-periodic",
   700  		"oldest-periodic",
   701  		"old-failed-trusted",
   702  	)
   703  	podsTrusted := []runtime.Object{
   704  		&corev1api.Pod{
   705  			ObjectMeta: metav1.ObjectMeta{
   706  				Name:      "old-failed-trusted",
   707  				Namespace: "ns",
   708  				Labels: map[string]string{
   709  					kube.CreatedByProw: "true",
   710  				},
   711  			},
   712  			Status: corev1api.PodStatus{
   713  				Phase:     corev1api.PodFailed,
   714  				StartTime: startTime(time.Now().Add(-maxPodAge).Add(-time.Second)),
   715  			},
   716  		},
   717  	}
   718  	deletedPodsTrusted := sets.New[string]("old-failed-trusted")
   719  
   720  	fpjc := &clientWrapper{
   721  		Client:          fakectrlruntimeclient.NewClientBuilder().WithRuntimeObjects(prowJobs...).Build(),
   722  		getOnlyProwJobs: map[string]*prowv1.ProwJob{"ns/get-only-prowjob": {}},
   723  	}
   724  	fkc := []*podClientWrapper{
   725  		{t: t, Client: fakectrlruntimeclient.NewClientBuilder().WithRuntimeObjects(pods...).Build()},
   726  		{t: t, Client: fakectrlruntimeclient.NewClientBuilder().WithRuntimeObjects(podsTrusted...).Build()},
   727  	}
   728  	fpc := map[string]ctrlruntimeclient.Client{"unreachable": unreachableCluster{}}
   729  	for idx, fakeClient := range fkc {
   730  		fpc[strconv.Itoa(idx)] = &podClientWrapper{t: t, Client: fakeClient}
   731  	}
   732  	// Run
   733  	c := controller{
   734  		logger:        logrus.WithField("component", "sinker"),
   735  		prowJobClient: fpjc,
   736  		podClients:    fpc,
   737  		config:        newFakeConfigAgent(newDefaultFakeSinkerConfig()).Config,
   738  	}
   739  	c.clean()
   740  	assertSetsEqual(deletedPods, fkc[0].deletedPods, t, "did not delete correct Pods")
   741  	assertSetsEqual(deletedPodsTrusted, fkc[1].deletedPods, t, "did not delete correct trusted Pods")
   742  
   743  	remainingProwJobs := &prowv1.ProwJobList{}
   744  	if err := fpjc.List(context.Background(), remainingProwJobs); err != nil {
   745  		t.Fatalf("failed to get remaining prowjobs: %v", err)
   746  	}
   747  	actuallyDeletedProwJobs := sets.Set[string]{}
   748  	for _, initalProwJob := range prowJobs {
   749  		actuallyDeletedProwJobs.Insert(initalProwJob.(metav1.Object).GetName())
   750  	}
   751  	for _, remainingProwJob := range remainingProwJobs.Items {
   752  		actuallyDeletedProwJobs.Delete(remainingProwJob.Name)
   753  	}
   754  	assertSetsEqual(deletedProwJobs, actuallyDeletedProwJobs, t, "did not delete correct ProwJobs")
   755  }
   756  
   757  func TestNotClean(t *testing.T) {
   758  
   759  	pods := []runtime.Object{
   760  		&corev1api.Pod{
   761  			ObjectMeta: metav1.ObjectMeta{
   762  				Name:      "job-complete-pod-succeeded",
   763  				Namespace: "ns",
   764  				Labels: map[string]string{
   765  					kube.CreatedByProw:  "true",
   766  					kube.ProwJobIDLabel: "job-complete",
   767  				},
   768  			},
   769  			Status: corev1api.PodStatus{
   770  				Phase:     corev1api.PodSucceeded,
   771  				StartTime: startTime(time.Now().Add(-maxPodAge).Add(-time.Second)),
   772  			},
   773  		},
   774  	}
   775  	podsExcluded := []runtime.Object{
   776  		&corev1api.Pod{
   777  			ObjectMeta: metav1.ObjectMeta{
   778  				Name:      "job-complete-pod-succeeded-on-excluded-cluster",
   779  				Namespace: "ns",
   780  				Labels: map[string]string{
   781  					kube.CreatedByProw:  "true",
   782  					kube.ProwJobIDLabel: "job-complete",
   783  				},
   784  			},
   785  			Status: corev1api.PodStatus{
   786  				Phase:     corev1api.PodSucceeded,
   787  				StartTime: startTime(time.Now().Add(-maxPodAge).Add(-time.Second)),
   788  			},
   789  		},
   790  	}
   791  	setComplete := func(d time.Duration) *metav1.Time {
   792  		completed := metav1.NewTime(time.Now().Add(d))
   793  		return &completed
   794  	}
   795  	prowJobs := []runtime.Object{
   796  		&prowv1.ProwJob{
   797  			ObjectMeta: metav1.ObjectMeta{
   798  				Name:      "job-complete",
   799  				Namespace: "ns",
   800  			},
   801  			Status: prowv1.ProwJobStatus{
   802  				StartTime:      metav1.NewTime(time.Now().Add(-maxProwJobAge).Add(-time.Second)),
   803  				CompletionTime: setComplete(-60 * time.Second),
   804  			},
   805  		},
   806  	}
   807  
   808  	deletedPods := sets.New[string](
   809  		"job-complete-pod-succeeded",
   810  	)
   811  
   812  	fpjc := &clientWrapper{
   813  		Client:          fakectrlruntimeclient.NewClientBuilder().WithRuntimeObjects(prowJobs...).Build(),
   814  		getOnlyProwJobs: map[string]*prowv1.ProwJob{"ns/get-only-prowjob": {}},
   815  	}
   816  	podClientValid := podClientWrapper{
   817  		t: t, Client: fakectrlruntimeclient.NewClientBuilder().WithRuntimeObjects(pods...).Build(),
   818  	}
   819  	podClientExcluded := podClientWrapper{
   820  		t: t, Client: fakectrlruntimeclient.NewClientBuilder().WithRuntimeObjects(podsExcluded...).Build(),
   821  	}
   822  	fpc := map[string]ctrlruntimeclient.Client{
   823  		"build-cluster-valid":    &podClientValid,
   824  		"build-cluster-excluded": &podClientExcluded,
   825  	}
   826  	// Run
   827  	fakeSinkerConfig := newDefaultFakeSinkerConfig()
   828  	fakeSinkerConfig.ExcludeClusters = []string{"build-cluster-excluded"}
   829  	fakeConfigAgent := newFakeConfigAgent(fakeSinkerConfig).Config
   830  	c := controller{
   831  		logger:        logrus.WithField("component", "sinker"),
   832  		prowJobClient: fpjc,
   833  		podClients:    fpc,
   834  		config:        fakeConfigAgent,
   835  	}
   836  	c.clean()
   837  	assertSetsEqual(deletedPods, podClientValid.deletedPods, t, "did not delete correct Pods")
   838  	assertSetsEqual(sets.Set[string]{}, podClientExcluded.deletedPods, t, "did not delete correct Pods")
   839  }
   840  
   841  func assertSetsEqual(expected, actual sets.Set[string], t *testing.T, prefix string) {
   842  	if expected.Equal(actual) {
   843  		return
   844  	}
   845  
   846  	if missing := expected.Difference(actual); missing.Len() > 0 {
   847  		t.Errorf("%s: missing expected: %v", prefix, sets.List(missing))
   848  	}
   849  	if extra := actual.Difference(expected); extra.Len() > 0 {
   850  		t.Errorf("%s: found unexpected: %v", prefix, sets.List(extra))
   851  	}
   852  }
   853  
   854  func TestFlags(t *testing.T) {
   855  	cases := []struct {
   856  		name     string
   857  		args     map[string]string
   858  		del      sets.Set[string]
   859  		expected func(*options)
   860  		err      bool
   861  	}{
   862  		{
   863  			name: "minimal flags work",
   864  		},
   865  		{
   866  			name: "explicitly set --config-path",
   867  			args: map[string]string{
   868  				"--config-path": "/random/path",
   869  			},
   870  			expected: func(o *options) {
   871  				o.config.ConfigPath = "/random/path"
   872  			},
   873  		},
   874  		{
   875  			name: "explicitly set --dry-run=false",
   876  			args: map[string]string{
   877  				"--dry-run": "false",
   878  			},
   879  			expected: func(o *options) {
   880  			},
   881  		},
   882  		{
   883  			name: "explicitly set --dry-run=true",
   884  			args: map[string]string{
   885  				"--dry-run": "true",
   886  			},
   887  			expected: func(o *options) {
   888  				o.dryRun = true
   889  			},
   890  		},
   891  		{
   892  			name: "dry run defaults to true",
   893  			args: map[string]string{},
   894  			del:  sets.New[string]("--dry-run"),
   895  			expected: func(o *options) {
   896  				o.dryRun = true
   897  			},
   898  		},
   899  	}
   900  
   901  	for _, tc := range cases {
   902  		t.Run(tc.name, func(t *testing.T) {
   903  			expected := &options{
   904  				config: configflagutil.ConfigOptions{
   905  					ConfigPathFlagName:                    "config-path",
   906  					JobConfigPathFlagName:                 "job-config-path",
   907  					ConfigPath:                            "yo",
   908  					SupplementalProwConfigsFileNameSuffix: "_prowconfig.yaml",
   909  					InRepoConfigCacheSize:                 200,
   910  				},
   911  				dryRun:                 false,
   912  				instrumentationOptions: flagutil.DefaultInstrumentationOptions(),
   913  			}
   914  			if tc.expected != nil {
   915  				tc.expected(expected)
   916  			}
   917  
   918  			argMap := map[string]string{
   919  				"--config-path": "yo",
   920  				"--dry-run":     "false",
   921  			}
   922  			for k, v := range tc.args {
   923  				argMap[k] = v
   924  			}
   925  			for k := range tc.del {
   926  				delete(argMap, k)
   927  			}
   928  
   929  			var args []string
   930  			for k, v := range argMap {
   931  				args = append(args, k+"="+v)
   932  			}
   933  			fs := flag.NewFlagSet("fake-flags", flag.PanicOnError)
   934  			actual := gatherOptions(fs, args...)
   935  			switch err := actual.Validate(); {
   936  			case err != nil:
   937  				if !tc.err {
   938  					t.Errorf("unexpected error: %v", err)
   939  				}
   940  			case tc.err:
   941  				t.Errorf("failed to receive expected error")
   942  			case !reflect.DeepEqual(*expected, actual):
   943  				t.Errorf("\n%#v\n != expected \n%#v\n", actual, *expected)
   944  			}
   945  		})
   946  	}
   947  }
   948  
   949  func TestDeletePodToleratesNotFound(t *testing.T) {
   950  	m := &sinkerReconciliationMetrics{
   951  		podsRemoved:      map[string]int{},
   952  		podRemovalErrors: map[string]int{},
   953  	}
   954  	c := &controller{config: newFakeConfigAgent(newDefaultFakeSinkerConfig()).Config}
   955  	l := logrus.NewEntry(logrus.New())
   956  	pod := &corev1api.Pod{
   957  		ObjectMeta: metav1.ObjectMeta{
   958  			Name:      "existing",
   959  			Namespace: "ns",
   960  			Labels: map[string]string{
   961  				kube.CreatedByProw:  "true",
   962  				kube.ProwJobIDLabel: "job-running",
   963  			},
   964  		},
   965  	}
   966  	client := fakectrlruntimeclient.NewClientBuilder().WithRuntimeObjects(pod).Build()
   967  
   968  	c.deletePod(l, &corev1api.Pod{}, "reason", client, m)
   969  	c.deletePod(l, pod, "reason", client, m)
   970  
   971  	if n := len(m.podRemovalErrors); n != 1 {
   972  		t.Errorf("Expected 1 pod removal errors, got %v", m.podRemovalErrors)
   973  	}
   974  	if n := len(m.podsRemoved); n != 1 {
   975  		t.Errorf("Expected 1 pod removal, got %v", m.podsRemoved)
   976  	}
   977  }
   978  
   979  type podClientWrapper struct {
   980  	t *testing.T
   981  	ctrlruntimeclient.Client
   982  	deletedPods sets.Set[string]
   983  }
   984  
   985  func (c *podClientWrapper) Delete(ctx context.Context, obj ctrlruntimeclient.Object, opts ...ctrlruntimeclient.DeleteOption) error {
   986  	var pod corev1api.Pod
   987  	name := types.NamespacedName{
   988  		Namespace: obj.(metav1.Object).GetNamespace(),
   989  		Name:      obj.(metav1.Object).GetName(),
   990  	}
   991  	if err := c.Get(ctx, name, &pod); err != nil {
   992  		return err
   993  	}
   994  	// The kube api allows this but we want to ensure in tests that we first clean up finalizers before deleting a pod
   995  	if len(pod.Finalizers) > 0 {
   996  		c.t.Errorf("attempting to delete pod %s that still has %v finalizers", pod.Name, pod.Finalizers)
   997  	}
   998  	if err := c.Client.Delete(ctx, obj, opts...); err != nil {
   999  		return err
  1000  	}
  1001  	if c.deletedPods == nil {
  1002  		c.deletedPods = sets.Set[string]{}
  1003  	}
  1004  	c.deletedPods.Insert(pod.Name)
  1005  	return nil
  1006  }
  1007  
  1008  type clientWrapper struct {
  1009  	ctrlruntimeclient.Client
  1010  	getOnlyProwJobs map[string]*prowv1.ProwJob
  1011  }
  1012  
  1013  func (c *clientWrapper) Get(ctx context.Context, key ctrlruntimeclient.ObjectKey, obj ctrlruntimeclient.Object, getOpts ...ctrlruntimeclient.GetOption) error {
  1014  	if pj, exists := c.getOnlyProwJobs[key.String()]; exists {
  1015  		*obj.(*prowv1.ProwJob) = *pj
  1016  		return nil
  1017  	}
  1018  	return c.Client.Get(ctx, key, obj, getOpts...)
  1019  }
  1020  
  1021  func TestGetConfigMapSize(t *testing.T) {
  1022  	toplevel, err := os.MkdirTemp("", "job-config")
  1023  	if err != nil {
  1024  		t.Fatal(err)
  1025  	}
  1026  	defer os.RemoveAll(toplevel)
  1027  
  1028  	timestampDir := filepath.Join(toplevel, "..2024_01_01")
  1029  	err = os.Mkdir(timestampDir, 0750)
  1030  	if err != nil && !os.IsExist(err) {
  1031  		t.Fatal(err)
  1032  	}
  1033  
  1034  	// Create files inside timestampDir, just like how K8s does it.
  1035  	file1 := filepath.Join(timestampDir, "key1")
  1036  	if err := os.WriteFile(file1, []byte("val1"), 0666); err != nil {
  1037  		t.Fatal(err)
  1038  	}
  1039  
  1040  	file2 := filepath.Join(timestampDir, "key2")
  1041  	if err := os.WriteFile(file2, []byte("val2"), 0666); err != nil {
  1042  		t.Fatal(err)
  1043  	}
  1044  
  1045  	file3 := filepath.Join(timestampDir, "key3")
  1046  	if err := os.WriteFile(file3, []byte("val3"), 0666); err != nil {
  1047  		t.Fatal(err)
  1048  	}
  1049  
  1050  	// Symlink ..data to point to timestampDir.
  1051  	dataDir := getDataDir(toplevel)
  1052  	os.Symlink(timestampDir, dataDir)
  1053  
  1054  	// Create symlinks at the toplevel that point to files in ..data
  1055  	// (again, like how K8s does it).
  1056  	os.Symlink(filepath.Join(dataDir, "key1"), filepath.Join(toplevel, "key1"))
  1057  	os.Symlink(filepath.Join(dataDir, "key2"), filepath.Join(toplevel, "key2"))
  1058  	os.Symlink(filepath.Join(dataDir, "key3"), filepath.Join(toplevel, "key3"))
  1059  
  1060  	gotBytes, err := getConfigMapSize(toplevel)
  1061  	if err != nil {
  1062  		t.Error(err)
  1063  	}
  1064  
  1065  	// Expect 12 bytes, because the 3 files each have 4 bytes of content.
  1066  	if gotBytes != 12 {
  1067  		t.Errorf("expected 12 bytes but got %v", gotBytes)
  1068  	}
  1069  }
  1070  
  1071  func TestGetConfigMapDirs(t *testing.T) {
  1072  	toplevel, err := os.MkdirTemp("", "job-config")
  1073  	if err != nil {
  1074  		t.Fatal(err)
  1075  	}
  1076  	defer os.RemoveAll(toplevel)
  1077  
  1078  	subdir1 := filepath.Join(toplevel, "part-1")
  1079  	if err := os.Mkdir(subdir1, 0750); err != nil && !os.IsExist(err) {
  1080  		t.Fatal(err)
  1081  	}
  1082  
  1083  	subdir2 := filepath.Join(toplevel, "part-2")
  1084  	if err := os.Mkdir(subdir2, 0750); err != nil && !os.IsExist(err) {
  1085  		t.Fatal(err)
  1086  	}
  1087  
  1088  	subdir3 := filepath.Join(toplevel, "part-3")
  1089  	if err := os.Mkdir(subdir3, 0750); err != nil && !os.IsExist(err) {
  1090  		t.Fatal(err)
  1091  	}
  1092  
  1093  	expected := []string{subdir1, subdir2, subdir3}
  1094  	dirs, err := getConfigMapDirs(toplevel)
  1095  	if err != nil {
  1096  		t.Fatal(err)
  1097  	}
  1098  
  1099  	if diff := cmp.Diff(dirs, expected); diff != "" {
  1100  		t.Fatal(diff)
  1101  	}
  1102  
  1103  	// Now create a "..data" symlink inside the toplevel dir. We now expect
  1104  	// getConfigMapDirs() to only give us back the toplevel directory itself,
  1105  	// because it should see the "..data" and treat the toplevel directory as
  1106  	// the only ConfigMap-mounted directory (that it is not partitioned into
  1107  	// multiple ConfigMap-mounted subdirectories).
  1108  	//
  1109  	// For purposes of this test the target of the symlink doesn't matter (we
  1110  	// just check that getConfigMapDirs() sees the "..data" symlink and treats
  1111  	// the toplevel folder as a single ConfigMap). However, getConfigMapDirs()
  1112  	// uses os.Stat() (instead of os.Lstat()) so in order to pass/fail the
  1113  	// existence check, we have to create a valid symlink with a target that
  1114  	// exists.
  1115  	dataDir := getDataDir(toplevel)
  1116  	if err := os.Symlink(toplevel, dataDir); err != nil {
  1117  		t.Fatal(err)
  1118  	}
  1119  
  1120  	expectedToplevelOnly := []string{toplevel}
  1121  	dirs, err = getConfigMapDirs(toplevel)
  1122  	if err != nil {
  1123  		t.Fatal(err)
  1124  	}
  1125  
  1126  	if diff := cmp.Diff(dirs, expectedToplevelOnly); diff != "" {
  1127  		t.Fatal(diff)
  1128  	}
  1129  }