sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/plank/controller_test.go (about)

     1  /*
     2  Copyright 2017 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 plank
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"net/http"
    24  	"net/http/httptest"
    25  	"reflect"
    26  	"strconv"
    27  	"sync"
    28  	"testing"
    29  	"text/template"
    30  	"time"
    31  
    32  	"github.com/google/go-cmp/cmp"
    33  	"github.com/sirupsen/logrus"
    34  	v1 "k8s.io/api/core/v1"
    35  	kapierrors "k8s.io/apimachinery/pkg/api/errors"
    36  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    37  	"k8s.io/apimachinery/pkg/runtime/schema"
    38  	"k8s.io/apimachinery/pkg/types"
    39  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    40  	"k8s.io/apimachinery/pkg/util/sets"
    41  	"k8s.io/utils/clock"
    42  	clocktesting "k8s.io/utils/clock/testing"
    43  	ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
    44  	fakectrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
    45  	"sigs.k8s.io/controller-runtime/pkg/event"
    46  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    47  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    48  	"sigs.k8s.io/prow/pkg/config"
    49  	"sigs.k8s.io/prow/pkg/kube"
    50  	"sigs.k8s.io/prow/pkg/pjutil"
    51  )
    52  
    53  type fca struct {
    54  	sync.Mutex
    55  	c *config.Config
    56  }
    57  
    58  const (
    59  	podPendingTimeout     = time.Hour
    60  	podRunningTimeout     = time.Hour * 2
    61  	podUnscheduledTimeout = time.Minute * 5
    62  )
    63  
    64  func newFakeConfigAgent(t *testing.T, maxConcurrency int, queueCapacities map[string]int) *fca {
    65  	presubmits := []config.Presubmit{
    66  		{
    67  			JobBase: config.JobBase{
    68  				Name: "test-bazel-build",
    69  			},
    70  		},
    71  		{
    72  			JobBase: config.JobBase{
    73  				Name: "test-e2e",
    74  			},
    75  		},
    76  		{
    77  			AlwaysRun: true,
    78  			JobBase: config.JobBase{
    79  				Name: "test-bazel-test",
    80  			},
    81  		},
    82  	}
    83  	if err := config.SetPresubmitRegexes(presubmits); err != nil {
    84  		t.Fatal(err)
    85  	}
    86  	presubmitMap := map[string][]config.Presubmit{
    87  		"kubernetes/kubernetes": presubmits,
    88  	}
    89  
    90  	return &fca{
    91  		c: &config.Config{
    92  			ProwConfig: config.ProwConfig{
    93  				ProwJobNamespace: "prowjobs",
    94  				PodNamespace:     "pods",
    95  				Plank: config.Plank{
    96  					Controller: config.Controller{
    97  						JobURLTemplate: template.Must(template.New("test").Parse("{{.ObjectMeta.Name}}/{{.Status.State}}")),
    98  						MaxConcurrency: maxConcurrency,
    99  						MaxGoroutines:  20,
   100  					},
   101  					JobQueueCapacities:    queueCapacities,
   102  					PodPendingTimeout:     &metav1.Duration{Duration: podPendingTimeout},
   103  					PodRunningTimeout:     &metav1.Duration{Duration: podRunningTimeout},
   104  					PodUnscheduledTimeout: &metav1.Duration{Duration: podUnscheduledTimeout},
   105  				},
   106  			},
   107  			JobConfig: config.JobConfig{
   108  				PresubmitsStatic: presubmitMap,
   109  			},
   110  		},
   111  	}
   112  }
   113  
   114  func (f *fca) Config() *config.Config {
   115  	f.Lock()
   116  	defer f.Unlock()
   117  	return f.c
   118  }
   119  
   120  func TestTerminateDupes(t *testing.T) {
   121  	now := time.Now()
   122  	nowFn := func() *metav1.Time {
   123  		reallyNow := metav1.NewTime(now)
   124  		return &reallyNow
   125  	}
   126  	type testCase struct {
   127  		Name string
   128  
   129  		PJs []prowapi.ProwJob
   130  
   131  		TerminatedPJs sets.Set[string]
   132  	}
   133  	testcases := []testCase{
   134  		{
   135  			Name: "terminate all duplicates",
   136  
   137  			PJs: []prowapi.ProwJob{
   138  				{
   139  					ObjectMeta: metav1.ObjectMeta{Name: "newest", Namespace: "prowjobs"},
   140  					Spec: prowapi.ProwJobSpec{
   141  						Type: prowapi.PresubmitJob,
   142  						Job:  "j1",
   143  						Refs: &prowapi.Refs{Pulls: []prowapi.Pull{{}}},
   144  					},
   145  					Status: prowapi.ProwJobStatus{
   146  						StartTime: metav1.NewTime(now.Add(-time.Minute)),
   147  					},
   148  				},
   149  				{
   150  					ObjectMeta: metav1.ObjectMeta{Name: "old", Namespace: "prowjobs"},
   151  					Spec: prowapi.ProwJobSpec{
   152  						Type: prowapi.PresubmitJob,
   153  						Job:  "j1",
   154  						Refs: &prowapi.Refs{Pulls: []prowapi.Pull{{}}},
   155  					},
   156  					Status: prowapi.ProwJobStatus{
   157  						StartTime: metav1.NewTime(now.Add(-time.Hour)),
   158  					},
   159  				},
   160  				{
   161  					ObjectMeta: metav1.ObjectMeta{Name: "older", Namespace: "prowjobs"},
   162  					Spec: prowapi.ProwJobSpec{
   163  						Type: prowapi.PresubmitJob,
   164  						Job:  "j1",
   165  						Refs: &prowapi.Refs{Pulls: []prowapi.Pull{{}}},
   166  					},
   167  					Status: prowapi.ProwJobStatus{
   168  						StartTime: metav1.NewTime(now.Add(-2 * time.Hour)),
   169  					},
   170  				},
   171  				{
   172  					ObjectMeta: metav1.ObjectMeta{Name: "complete", Namespace: "prowjobs"},
   173  					Spec: prowapi.ProwJobSpec{
   174  						Type: prowapi.PresubmitJob,
   175  						Job:  "j1",
   176  						Refs: &prowapi.Refs{Pulls: []prowapi.Pull{{}}},
   177  					},
   178  					Status: prowapi.ProwJobStatus{
   179  						StartTime:      metav1.NewTime(now.Add(-3 * time.Hour)),
   180  						CompletionTime: nowFn(),
   181  					},
   182  				},
   183  				{
   184  					ObjectMeta: metav1.ObjectMeta{Name: "newest_j2", Namespace: "prowjobs"},
   185  					Spec: prowapi.ProwJobSpec{
   186  						Type: prowapi.PresubmitJob,
   187  						Job:  "j2",
   188  						Refs: &prowapi.Refs{Pulls: []prowapi.Pull{{}}},
   189  					},
   190  					Status: prowapi.ProwJobStatus{
   191  						StartTime: metav1.NewTime(now.Add(-time.Minute)),
   192  					},
   193  				},
   194  				{
   195  					ObjectMeta: metav1.ObjectMeta{Name: "old_j2", Namespace: "prowjobs"},
   196  					Spec: prowapi.ProwJobSpec{
   197  						Type: prowapi.PresubmitJob,
   198  						Job:  "j2",
   199  						Refs: &prowapi.Refs{Pulls: []prowapi.Pull{{}}},
   200  					},
   201  					Status: prowapi.ProwJobStatus{
   202  						StartTime: metav1.NewTime(now.Add(-time.Hour)),
   203  					},
   204  				},
   205  				{
   206  					ObjectMeta: metav1.ObjectMeta{Name: "old_j3", Namespace: "prowjobs"},
   207  					Spec: prowapi.ProwJobSpec{
   208  						Type: prowapi.PresubmitJob,
   209  						Job:  "j3",
   210  						Refs: &prowapi.Refs{Pulls: []prowapi.Pull{{}}},
   211  					},
   212  					Status: prowapi.ProwJobStatus{
   213  						StartTime: metav1.NewTime(now.Add(-time.Hour)),
   214  					},
   215  				},
   216  				{
   217  					ObjectMeta: metav1.ObjectMeta{Name: "new_j3", Namespace: "prowjobs"},
   218  					Spec: prowapi.ProwJobSpec{
   219  						Type: prowapi.PresubmitJob,
   220  						Job:  "j3",
   221  						Refs: &prowapi.Refs{Pulls: []prowapi.Pull{{}}},
   222  					},
   223  					Status: prowapi.ProwJobStatus{
   224  						StartTime: metav1.NewTime(now.Add(-time.Minute)),
   225  					},
   226  				},
   227  			},
   228  
   229  			TerminatedPJs: sets.New[string]("old", "older", "old_j2", "old_j3"),
   230  		},
   231  		{
   232  			Name: "should also terminate pods",
   233  
   234  			PJs: []prowapi.ProwJob{
   235  				{
   236  					ObjectMeta: metav1.ObjectMeta{Name: "newest", Namespace: "prowjobs"},
   237  					Spec: prowapi.ProwJobSpec{
   238  						Type:    prowapi.PresubmitJob,
   239  						Job:     "j1",
   240  						Refs:    &prowapi.Refs{Pulls: []prowapi.Pull{{}}},
   241  						PodSpec: &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
   242  					},
   243  					Status: prowapi.ProwJobStatus{
   244  						StartTime: metav1.NewTime(now.Add(-time.Minute)),
   245  					},
   246  				},
   247  				{
   248  					ObjectMeta: metav1.ObjectMeta{Name: "old", Namespace: "prowjobs"},
   249  					Spec: prowapi.ProwJobSpec{
   250  						Type:    prowapi.PresubmitJob,
   251  						Job:     "j1",
   252  						Refs:    &prowapi.Refs{Pulls: []prowapi.Pull{{}}},
   253  						PodSpec: &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
   254  					},
   255  					Status: prowapi.ProwJobStatus{
   256  						StartTime: metav1.NewTime(now.Add(-time.Hour)),
   257  					},
   258  				},
   259  			},
   260  
   261  			TerminatedPJs: sets.New[string]("old"),
   262  		},
   263  	}
   264  
   265  	for _, tc := range testcases {
   266  		t.Run(tc.Name, func(t *testing.T) {
   267  			builder := fakectrlruntimeclient.NewClientBuilder()
   268  			for i := range tc.PJs {
   269  				builder.WithRuntimeObjects(&tc.PJs[i])
   270  			}
   271  			fakeProwJobClient := &patchTrackingFakeClient{
   272  				Client: builder.Build(),
   273  			}
   274  			fca := &fca{
   275  				c: &config.Config{
   276  					ProwConfig: config.ProwConfig{
   277  						ProwJobNamespace: "prowjobs",
   278  						PodNamespace:     "pods",
   279  					},
   280  				},
   281  			}
   282  			log := logrus.NewEntry(logrus.StandardLogger())
   283  
   284  			r := &reconciler{
   285  				pjClient: fakeProwJobClient,
   286  				log:      log,
   287  				config:   fca.Config,
   288  				clock:    clock.RealClock{},
   289  			}
   290  			for _, pj := range tc.PJs {
   291  				res, err := r.reconcile(context.Background(), &pj)
   292  				if res != nil {
   293  					err = utilerrors.NewAggregate([]error{err, fmt.Errorf("expected reconcile.Result to be nil, was %v", res)})
   294  				}
   295  				if err != nil {
   296  					t.Fatalf("Error terminating dupes: %v", err)
   297  				}
   298  			}
   299  
   300  			observedCompletedProwJobs := fakeProwJobClient.patched
   301  			if missing := tc.TerminatedPJs.Difference(observedCompletedProwJobs); missing.Len() > 0 {
   302  				t.Errorf("did not delete expected prowJobs: %v", sets.List(missing))
   303  			}
   304  			if extra := observedCompletedProwJobs.Difference(tc.TerminatedPJs); extra.Len() > 0 {
   305  				t.Errorf("found unexpectedly deleted prowJobs: %v", sets.List(extra))
   306  			}
   307  		})
   308  	}
   309  }
   310  
   311  func handleTot(w http.ResponseWriter, r *http.Request) {
   312  	fmt.Fprint(w, "0987654321")
   313  }
   314  
   315  func TestSyncTriggeredJobs(t *testing.T) {
   316  	fakeClock := clocktesting.NewFakeClock(time.Now().Truncate(1 * time.Second))
   317  	pendingTime := metav1.NewTime(fakeClock.Now())
   318  
   319  	type testCase struct {
   320  		Name string
   321  
   322  		PJ             prowapi.ProwJob
   323  		PendingJobs    map[string]int
   324  		MaxConcurrency int
   325  		Pods           map[string][]v1.Pod
   326  		PodErr         error
   327  
   328  		ExpectedState       prowapi.ProwJobState
   329  		ExpectedPodHasName  bool
   330  		ExpectedNumPods     map[string]int
   331  		ExpectedCreatedPJs  int
   332  		ExpectedComplete    bool
   333  		ExpectedURL         string
   334  		ExpectedBuildID     string
   335  		ExpectError         bool
   336  		ExpectedPendingTime *metav1.Time
   337  	}
   338  
   339  	testcases := []testCase{
   340  		{
   341  			Name: "start new pod",
   342  			PJ: prowapi.ProwJob{
   343  				ObjectMeta: metav1.ObjectMeta{
   344  					Name:      "blabla",
   345  					Namespace: "prowjobs",
   346  				},
   347  				Spec: prowapi.ProwJobSpec{
   348  					Job:     "boop",
   349  					Type:    prowapi.PeriodicJob,
   350  					PodSpec: &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
   351  				},
   352  				Status: prowapi.ProwJobStatus{
   353  					State: prowapi.TriggeredState,
   354  				},
   355  			},
   356  			Pods:                map[string][]v1.Pod{"default": {}},
   357  			ExpectedState:       prowapi.PendingState,
   358  			ExpectedPendingTime: &pendingTime,
   359  			ExpectedPodHasName:  true,
   360  			ExpectedNumPods:     map[string]int{"default": 1},
   361  			ExpectedURL:         "blabla/pending",
   362  			ExpectedBuildID:     "0987654321",
   363  		},
   364  		{
   365  			Name: "pod with a max concurrency of 1",
   366  			PJ: prowapi.ProwJob{
   367  				ObjectMeta: metav1.ObjectMeta{
   368  					Name:              "blabla",
   369  					Namespace:         "prowjobs",
   370  					CreationTimestamp: metav1.Now(),
   371  				},
   372  				Spec: prowapi.ProwJobSpec{
   373  					Job:            "same",
   374  					Type:           prowapi.PeriodicJob,
   375  					MaxConcurrency: 1,
   376  					PodSpec:        &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
   377  				},
   378  				Status: prowapi.ProwJobStatus{
   379  					State: prowapi.TriggeredState,
   380  				},
   381  			},
   382  			PendingJobs: map[string]int{
   383  				"same": 1,
   384  			},
   385  			Pods: map[string][]v1.Pod{
   386  				"default": {
   387  					{
   388  						ObjectMeta: metav1.ObjectMeta{
   389  							Name:      "same-42",
   390  							Namespace: "pods",
   391  						},
   392  						Status: v1.PodStatus{
   393  							Phase: v1.PodRunning,
   394  						},
   395  					},
   396  				},
   397  			},
   398  			ExpectedState:   prowapi.TriggeredState,
   399  			ExpectedNumPods: map[string]int{"default": 1},
   400  		},
   401  		{
   402  			Name: "trusted pod with a max concurrency of 1",
   403  			PJ: prowapi.ProwJob{
   404  				ObjectMeta: metav1.ObjectMeta{
   405  					Name:              "blabla",
   406  					Namespace:         "prowjobs",
   407  					CreationTimestamp: metav1.Now(),
   408  				},
   409  				Spec: prowapi.ProwJobSpec{
   410  					Job:            "same",
   411  					Type:           prowapi.PeriodicJob,
   412  					Cluster:        "trusted",
   413  					MaxConcurrency: 1,
   414  					PodSpec:        &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
   415  				},
   416  				Status: prowapi.ProwJobStatus{
   417  					State: prowapi.TriggeredState,
   418  				},
   419  			},
   420  			PendingJobs: map[string]int{
   421  				"same": 1,
   422  			},
   423  			Pods: map[string][]v1.Pod{
   424  				"trusted": {
   425  					{
   426  						ObjectMeta: metav1.ObjectMeta{
   427  							Name:      "same-42",
   428  							Namespace: "pods",
   429  						},
   430  						Status: v1.PodStatus{
   431  							Phase: v1.PodRunning,
   432  						},
   433  					},
   434  				},
   435  			},
   436  			ExpectedState:   prowapi.TriggeredState,
   437  			ExpectedNumPods: map[string]int{"trusted": 1},
   438  		},
   439  		{
   440  			Name: "trusted pod with a max concurrency of 1 (can start)",
   441  			PJ: prowapi.ProwJob{
   442  				ObjectMeta: metav1.ObjectMeta{
   443  					Name:      "some",
   444  					Namespace: "prowjobs",
   445  				},
   446  				Spec: prowapi.ProwJobSpec{
   447  					Job:            "some",
   448  					Type:           prowapi.PeriodicJob,
   449  					Cluster:        "trusted",
   450  					MaxConcurrency: 1,
   451  					PodSpec:        &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
   452  				},
   453  				Status: prowapi.ProwJobStatus{
   454  					State: prowapi.TriggeredState,
   455  				},
   456  			},
   457  			Pods: map[string][]v1.Pod{
   458  				"default": {
   459  					{
   460  						ObjectMeta: metav1.ObjectMeta{
   461  							Name:      "other-42",
   462  							Namespace: "pods",
   463  						},
   464  						Status: v1.PodStatus{
   465  							Phase: v1.PodRunning,
   466  						},
   467  					},
   468  				},
   469  				"trusted": {},
   470  			},
   471  			ExpectedState:       prowapi.PendingState,
   472  			ExpectedNumPods:     map[string]int{"default": 1, "trusted": 1},
   473  			ExpectedPodHasName:  true,
   474  			ExpectedPendingTime: &pendingTime,
   475  			ExpectedURL:         "some/pending",
   476  		},
   477  		{
   478  			Name: "do not exceed global maxconcurrency",
   479  			PJ: prowapi.ProwJob{
   480  				ObjectMeta: metav1.ObjectMeta{
   481  					Name:      "beer",
   482  					Namespace: "prowjobs",
   483  				},
   484  				Spec: prowapi.ProwJobSpec{
   485  					Job:     "same",
   486  					Type:    prowapi.PeriodicJob,
   487  					PodSpec: &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
   488  				},
   489  				Status: prowapi.ProwJobStatus{
   490  					State: prowapi.TriggeredState,
   491  				},
   492  			},
   493  			MaxConcurrency: 20,
   494  			PendingJobs:    map[string]int{"motherearth": 10, "allagash": 8, "krusovice": 2},
   495  			ExpectedState:  prowapi.TriggeredState,
   496  		},
   497  		{
   498  			Name: "global maxconcurrency allows new jobs when possible",
   499  			PJ: prowapi.ProwJob{
   500  				ObjectMeta: metav1.ObjectMeta{
   501  					Name:      "beer",
   502  					Namespace: "prowjobs",
   503  				},
   504  				Spec: prowapi.ProwJobSpec{
   505  					Job:     "same",
   506  					Type:    prowapi.PeriodicJob,
   507  					PodSpec: &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
   508  				},
   509  				Status: prowapi.ProwJobStatus{
   510  					State: prowapi.TriggeredState,
   511  				},
   512  			},
   513  			Pods:                map[string][]v1.Pod{"default": {}},
   514  			MaxConcurrency:      21,
   515  			PendingJobs:         map[string]int{"motherearth": 10, "allagash": 8, "krusovice": 2},
   516  			ExpectedState:       prowapi.PendingState,
   517  			ExpectedNumPods:     map[string]int{"default": 1},
   518  			ExpectedURL:         "beer/pending",
   519  			ExpectedPendingTime: &pendingTime,
   520  		},
   521  		{
   522  			Name: "unprocessable prow job",
   523  			PJ: prowapi.ProwJob{
   524  				ObjectMeta: metav1.ObjectMeta{
   525  					Name:      "beer",
   526  					Namespace: "prowjobs",
   527  				},
   528  				Spec: prowapi.ProwJobSpec{
   529  					Job:     "boop",
   530  					Type:    prowapi.PeriodicJob,
   531  					PodSpec: &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
   532  				},
   533  				Status: prowapi.ProwJobStatus{
   534  					State: prowapi.TriggeredState,
   535  				},
   536  			},
   537  			Pods: map[string][]v1.Pod{"default": {}},
   538  			PodErr: &kapierrors.StatusError{ErrStatus: metav1.Status{
   539  				Status: metav1.StatusFailure,
   540  				Code:   http.StatusUnprocessableEntity,
   541  				Reason: metav1.StatusReasonInvalid,
   542  			}},
   543  			ExpectedState:    prowapi.ErrorState,
   544  			ExpectedComplete: true,
   545  		},
   546  		{
   547  			Name: "forbidden prow job",
   548  			PJ: prowapi.ProwJob{
   549  				ObjectMeta: metav1.ObjectMeta{
   550  					Name:      "beer",
   551  					Namespace: "prowjobs",
   552  				},
   553  				Spec: prowapi.ProwJobSpec{
   554  					Job:     "boop",
   555  					Type:    prowapi.PeriodicJob,
   556  					PodSpec: &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
   557  				},
   558  				Status: prowapi.ProwJobStatus{
   559  					State: prowapi.TriggeredState,
   560  				},
   561  			},
   562  			Pods: map[string][]v1.Pod{"default": {}},
   563  			PodErr: &kapierrors.StatusError{ErrStatus: metav1.Status{
   564  				Status: metav1.StatusFailure,
   565  				Code:   http.StatusForbidden,
   566  				Reason: metav1.StatusReasonForbidden,
   567  			}},
   568  			ExpectedState:    prowapi.ErrorState,
   569  			ExpectedComplete: true,
   570  		},
   571  		{
   572  			Name: "conflict error starting pod",
   573  			PJ: prowapi.ProwJob{
   574  				ObjectMeta: metav1.ObjectMeta{
   575  					Name:      "beer",
   576  					Namespace: "prowjobs",
   577  				},
   578  				Spec: prowapi.ProwJobSpec{
   579  					Job:     "boop",
   580  					Type:    prowapi.PeriodicJob,
   581  					PodSpec: &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
   582  				},
   583  				Status: prowapi.ProwJobStatus{
   584  					State: prowapi.TriggeredState,
   585  				},
   586  			},
   587  			Pods: map[string][]v1.Pod{"default": {}},
   588  			PodErr: &kapierrors.StatusError{ErrStatus: metav1.Status{
   589  				Status: metav1.StatusFailure,
   590  				Code:   http.StatusConflict,
   591  				Reason: metav1.StatusReasonAlreadyExists,
   592  			}},
   593  			ExpectedState:    prowapi.ErrorState,
   594  			ExpectedComplete: true,
   595  		},
   596  		{
   597  			Name: "unknown error starting pod",
   598  			PJ: prowapi.ProwJob{
   599  				ObjectMeta: metav1.ObjectMeta{
   600  					Name:      "beer",
   601  					Namespace: "prowjobs",
   602  				},
   603  				Spec: prowapi.ProwJobSpec{
   604  					Job:     "boop",
   605  					Type:    prowapi.PeriodicJob,
   606  					PodSpec: &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
   607  				},
   608  				Status: prowapi.ProwJobStatus{
   609  					State: prowapi.TriggeredState,
   610  				},
   611  			},
   612  			PodErr:        errors.New("no way unknown jose"),
   613  			ExpectedState: prowapi.TriggeredState,
   614  			ExpectError:   true,
   615  		},
   616  		{
   617  			Name: "running pod, failed prowjob update",
   618  			PJ: prowapi.ProwJob{
   619  				ObjectMeta: metav1.ObjectMeta{
   620  					Name:      "foo",
   621  					Namespace: "prowjobs",
   622  				},
   623  				Spec: prowapi.ProwJobSpec{
   624  					Job:     "boop",
   625  					Type:    prowapi.PeriodicJob,
   626  					PodSpec: &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
   627  				},
   628  				Status: prowapi.ProwJobStatus{
   629  					State: prowapi.TriggeredState,
   630  				},
   631  			},
   632  			Pods: map[string][]v1.Pod{
   633  				"default": {
   634  					{
   635  						ObjectMeta: metav1.ObjectMeta{
   636  							Name:      "foo",
   637  							Namespace: "pods",
   638  							Labels: map[string]string{
   639  								kube.ProwBuildIDLabel: "0987654321",
   640  							},
   641  						},
   642  						Status: v1.PodStatus{
   643  							Phase: v1.PodRunning,
   644  						},
   645  					},
   646  				},
   647  			},
   648  			ExpectedState:       prowapi.PendingState,
   649  			ExpectedNumPods:     map[string]int{"default": 1},
   650  			ExpectedPendingTime: &pendingTime,
   651  			ExpectedURL:         "foo/pending",
   652  			ExpectedBuildID:     "0987654321",
   653  			ExpectedPodHasName:  true,
   654  		},
   655  		{
   656  			Name: "running pod, failed prowjob update, backwards compatible on pods with build label not set",
   657  			PJ: prowapi.ProwJob{
   658  				ObjectMeta: metav1.ObjectMeta{
   659  					Name:      "foo",
   660  					Namespace: "prowjobs",
   661  				},
   662  				Spec: prowapi.ProwJobSpec{
   663  					Job:     "boop",
   664  					Type:    prowapi.PeriodicJob,
   665  					PodSpec: &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
   666  				},
   667  				Status: prowapi.ProwJobStatus{
   668  					State: prowapi.TriggeredState,
   669  				},
   670  			},
   671  			Pods: map[string][]v1.Pod{
   672  				"default": {
   673  					{
   674  						ObjectMeta: metav1.ObjectMeta{
   675  							Name:      "foo",
   676  							Namespace: "pods",
   677  							Labels: map[string]string{
   678  								kube.ProwBuildIDLabel: "",
   679  							},
   680  						},
   681  						Spec: v1.PodSpec{
   682  							Containers: []v1.Container{
   683  								{
   684  									Name: "test-name",
   685  									Env: []v1.EnvVar{
   686  										{
   687  											Name:  "BUILD_ID",
   688  											Value: "0987654321",
   689  										},
   690  									},
   691  								},
   692  							},
   693  						},
   694  						Status: v1.PodStatus{
   695  							Phase: v1.PodRunning,
   696  						},
   697  					},
   698  				},
   699  			},
   700  			ExpectedState:       prowapi.PendingState,
   701  			ExpectedNumPods:     map[string]int{"default": 1},
   702  			ExpectedPendingTime: &pendingTime,
   703  			ExpectedURL:         "foo/pending",
   704  			ExpectedBuildID:     "0987654321",
   705  			ExpectedPodHasName:  true,
   706  		},
   707  	}
   708  
   709  	for _, tc := range testcases {
   710  		t.Run(tc.Name, func(t *testing.T) {
   711  			totServ := httptest.NewServer(http.HandlerFunc(handleTot))
   712  			defer totServ.Close()
   713  			pm := make(map[string]v1.Pod)
   714  			for _, pods := range tc.Pods {
   715  				for i := range pods {
   716  					pm[pods[i].ObjectMeta.Name] = pods[i]
   717  				}
   718  			}
   719  			tc.PJ.Spec.Agent = prowapi.KubernetesAgent
   720  			fakeProwJobClient := fakectrlruntimeclient.NewClientBuilder().WithRuntimeObjects(&tc.PJ).Build()
   721  			buildClients := map[string]buildClient{}
   722  			for alias, pods := range tc.Pods {
   723  				builder := fakectrlruntimeclient.NewClientBuilder()
   724  				for i := range pods {
   725  					builder.WithRuntimeObjects(&pods[i])
   726  				}
   727  				fakeClient := &clientWrapper{
   728  					Client:      builder.Build(),
   729  					createError: tc.PodErr,
   730  				}
   731  				buildClients[alias] = buildClient{
   732  					Client: fakeClient,
   733  				}
   734  			}
   735  			if _, exists := buildClients[prowapi.DefaultClusterAlias]; !exists {
   736  				buildClients[prowapi.DefaultClusterAlias] = buildClient{
   737  					Client: &clientWrapper{
   738  						Client:      fakectrlruntimeclient.NewClientBuilder().Build(),
   739  						createError: tc.PodErr,
   740  					},
   741  				}
   742  			}
   743  
   744  			for jobName, numJobsToCreate := range tc.PendingJobs {
   745  				for i := 0; i < numJobsToCreate; i++ {
   746  					if err := fakeProwJobClient.Create(context.Background(), &prowapi.ProwJob{
   747  						ObjectMeta: metav1.ObjectMeta{
   748  							Name:      fmt.Sprintf("%s-%d", jobName, i),
   749  							Namespace: "prowjobs",
   750  						},
   751  						Spec: prowapi.ProwJobSpec{
   752  							Agent: prowapi.KubernetesAgent,
   753  							Job:   jobName,
   754  						},
   755  					}); err != nil {
   756  						t.Fatalf("failed to create prowJob: %v", err)
   757  					}
   758  				}
   759  			}
   760  			r := &reconciler{
   761  				pjClient:     fakeProwJobClient,
   762  				buildClients: buildClients,
   763  				log:          logrus.NewEntry(logrus.StandardLogger()),
   764  				config:       newFakeConfigAgent(t, tc.MaxConcurrency, nil).Config,
   765  				totURL:       totServ.URL,
   766  				clock:        fakeClock,
   767  			}
   768  			pj := tc.PJ.DeepCopy()
   769  			pj.UID = types.UID("under-test")
   770  			if _, err := r.syncTriggeredJob(context.Background(), pj); (err != nil) != tc.ExpectError {
   771  				if tc.ExpectError {
   772  					t.Errorf("for case %q expected an error, but got none", tc.Name)
   773  				} else {
   774  					t.Errorf("for case %q got an unexpected error: %v", tc.Name, err)
   775  				}
   776  				return
   777  			}
   778  			// In PlankV2 we throw them all into the same client and then count the resulting number
   779  			for _, pendingJobs := range tc.PendingJobs {
   780  				tc.ExpectedCreatedPJs += pendingJobs
   781  			}
   782  
   783  			actualProwJobs := &prowapi.ProwJobList{}
   784  			if err := fakeProwJobClient.List(context.Background(), actualProwJobs); err != nil {
   785  				t.Errorf("could not list prowJobs from the client: %v", err)
   786  			}
   787  			if len(actualProwJobs.Items) != tc.ExpectedCreatedPJs+1 {
   788  				t.Errorf("got %d created prowjobs, expected %d", len(actualProwJobs.Items)-1, tc.ExpectedCreatedPJs)
   789  			}
   790  			var actual prowapi.ProwJob
   791  			if err := fakeProwJobClient.Get(context.Background(), types.NamespacedName{Namespace: tc.PJ.Namespace, Name: tc.PJ.Name}, &actual); err != nil {
   792  				t.Errorf("failed to get prowjob from client: %v", err)
   793  			}
   794  			if actual.Status.State != tc.ExpectedState {
   795  				t.Errorf("expected state %v, got state %v", tc.ExpectedState, actual.Status.State)
   796  			}
   797  			if !reflect.DeepEqual(actual.Status.PendingTime, tc.ExpectedPendingTime) {
   798  				t.Errorf("got pending time %v, expected %v", actual.Status.PendingTime, tc.ExpectedPendingTime)
   799  			}
   800  			if (actual.Status.PodName == "") && tc.ExpectedPodHasName {
   801  				t.Errorf("got no pod name, expected one")
   802  			}
   803  			if tc.ExpectedBuildID != "" && actual.Status.BuildID != tc.ExpectedBuildID {
   804  				t.Errorf("expected BuildID: %q, got: %q", tc.ExpectedBuildID, actual.Status.BuildID)
   805  			}
   806  			for alias, expected := range tc.ExpectedNumPods {
   807  				actualPods := &v1.PodList{}
   808  				if err := buildClients[alias].List(context.Background(), actualPods); err != nil {
   809  					t.Errorf("could not list pods from the client: %v", err)
   810  				}
   811  				if got := len(actualPods.Items); got != expected {
   812  					t.Errorf("got %d pods for alias %q, but expected %d", got, alias, expected)
   813  				}
   814  			}
   815  			if actual.Complete() != tc.ExpectedComplete {
   816  				t.Error("got wrong completion")
   817  			}
   818  		})
   819  	}
   820  }
   821  
   822  func startTime(s time.Time) *metav1.Time {
   823  	start := metav1.NewTime(s)
   824  	return &start
   825  }
   826  
   827  func TestSyncPendingJob(t *testing.T) {
   828  	type testCase struct {
   829  		Name string
   830  
   831  		PJ   prowapi.ProwJob
   832  		Pods []v1.Pod
   833  		Err  error
   834  
   835  		expectedReconcileResult       *reconcile.Result
   836  		ExpectedState                 prowapi.ProwJobState
   837  		ExpectedNumPods               int
   838  		ExpectedComplete              bool
   839  		ExpectedCreatedPJs            int
   840  		ExpectedReport                bool
   841  		ExpectedURL                   string
   842  		ExpectedBuildID               string
   843  		ExpectedPodRunningTimeout     *metav1.Duration
   844  		ExpectedPodPendingTimeout     *metav1.Duration
   845  		ExpectedPodUnscheduledTimeout *metav1.Duration
   846  	}
   847  	testcases := []testCase{
   848  		{
   849  			Name: "reset when pod goes missing",
   850  			PJ: prowapi.ProwJob{
   851  				ObjectMeta: metav1.ObjectMeta{
   852  					Name:      "boop-41",
   853  					Namespace: "prowjobs",
   854  				},
   855  				Spec: prowapi.ProwJobSpec{
   856  					Type:    prowapi.PostsubmitJob,
   857  					PodSpec: &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
   858  					Refs:    &prowapi.Refs{Org: "fejtaverse"},
   859  				},
   860  				Status: prowapi.ProwJobStatus{
   861  					State:   prowapi.PendingState,
   862  					PodName: "boop-41",
   863  				},
   864  			},
   865  			ExpectedState:   prowapi.PendingState,
   866  			ExpectedReport:  true,
   867  			ExpectedNumPods: 1,
   868  			ExpectedURL:     "boop-41/pending",
   869  			ExpectedBuildID: "0987654321",
   870  		},
   871  		{
   872  			Name: "delete pod in unknown state",
   873  			PJ: prowapi.ProwJob{
   874  				ObjectMeta: metav1.ObjectMeta{
   875  					Name:      "boop-41",
   876  					Namespace: "prowjobs",
   877  				},
   878  				Spec: prowapi.ProwJobSpec{
   879  					PodSpec: &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
   880  				},
   881  				Status: prowapi.ProwJobStatus{
   882  					State:   prowapi.PendingState,
   883  					PodName: "boop-41",
   884  				},
   885  			},
   886  			Pods: []v1.Pod{
   887  				{
   888  					ObjectMeta: metav1.ObjectMeta{
   889  						Name:      "boop-41",
   890  						Namespace: "pods",
   891  					},
   892  					Status: v1.PodStatus{
   893  						Phase: v1.PodUnknown,
   894  					},
   895  				},
   896  			},
   897  			ExpectedState:   prowapi.PendingState,
   898  			ExpectedNumPods: 0,
   899  		},
   900  		{
   901  			Name: "delete pod in unknown state with gcsreporter finalizer",
   902  			PJ: prowapi.ProwJob{
   903  				ObjectMeta: metav1.ObjectMeta{
   904  					Name:      "boop-41",
   905  					Namespace: "prowjobs",
   906  				},
   907  				Spec: prowapi.ProwJobSpec{
   908  					PodSpec: &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
   909  				},
   910  				Status: prowapi.ProwJobStatus{
   911  					State:   prowapi.PendingState,
   912  					PodName: "boop-41",
   913  				},
   914  			},
   915  			Pods: []v1.Pod{
   916  				{
   917  					ObjectMeta: metav1.ObjectMeta{
   918  						Name:       "boop-41",
   919  						Namespace:  "pods",
   920  						Finalizers: []string{"prow.x-k8s.io/gcsk8sreporter"},
   921  					},
   922  					Status: v1.PodStatus{
   923  						Phase: v1.PodUnknown,
   924  					},
   925  				},
   926  			},
   927  			ExpectedState:   prowapi.PendingState,
   928  			ExpectedNumPods: 0,
   929  		},
   930  		{
   931  			Name: "succeeded pod",
   932  			PJ: prowapi.ProwJob{
   933  				ObjectMeta: metav1.ObjectMeta{
   934  					Name:      "boop-42",
   935  					Namespace: "prowjobs",
   936  				},
   937  				Spec: prowapi.ProwJobSpec{
   938  					Type:    prowapi.BatchJob,
   939  					PodSpec: &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
   940  					Refs:    &prowapi.Refs{Org: "fejtaverse"},
   941  				},
   942  				Status: prowapi.ProwJobStatus{
   943  					State:   prowapi.PendingState,
   944  					PodName: "boop-42",
   945  				},
   946  			},
   947  			Pods: []v1.Pod{
   948  				{
   949  					ObjectMeta: metav1.ObjectMeta{
   950  						Name:      "boop-42",
   951  						Namespace: "pods",
   952  					},
   953  					Status: v1.PodStatus{
   954  						Phase: v1.PodSucceeded,
   955  					},
   956  				},
   957  			},
   958  			ExpectedComplete:   true,
   959  			ExpectedState:      prowapi.SuccessState,
   960  			ExpectedNumPods:    1,
   961  			ExpectedCreatedPJs: 0,
   962  			ExpectedURL:        "boop-42/success",
   963  		},
   964  		{
   965  			Name: "succeeded pod with unfinished containers",
   966  			PJ: prowapi.ProwJob{
   967  				ObjectMeta: metav1.ObjectMeta{
   968  					Name:      "boop-42",
   969  					Namespace: "prowjobs",
   970  				},
   971  				Spec: prowapi.ProwJobSpec{
   972  					Type:    prowapi.BatchJob,
   973  					PodSpec: &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
   974  					Refs:    &prowapi.Refs{Org: "fejtaverse"},
   975  				},
   976  				Status: prowapi.ProwJobStatus{
   977  					State:   prowapi.PendingState,
   978  					PodName: "boop-42",
   979  				},
   980  			},
   981  			Pods: []v1.Pod{
   982  				{
   983  					ObjectMeta: metav1.ObjectMeta{
   984  						Name:      "boop-42",
   985  						Namespace: "pods",
   986  					},
   987  					Status: v1.PodStatus{
   988  						Phase:             v1.PodSucceeded,
   989  						ContainerStatuses: []v1.ContainerStatus{{LastTerminationState: v1.ContainerState{Terminated: &v1.ContainerStateTerminated{}}}},
   990  					},
   991  				},
   992  			},
   993  			ExpectedComplete:   true,
   994  			ExpectedState:      prowapi.ErrorState,
   995  			ExpectedNumPods:    1,
   996  			ExpectedCreatedPJs: 0,
   997  			ExpectedURL:        "boop-42/success",
   998  		},
   999  		{
  1000  			Name: "succeeded pod with unfinished initcontainers",
  1001  			PJ: prowapi.ProwJob{
  1002  				ObjectMeta: metav1.ObjectMeta{
  1003  					Name:      "boop-42",
  1004  					Namespace: "prowjobs",
  1005  				},
  1006  				Spec: prowapi.ProwJobSpec{
  1007  					Type:    prowapi.BatchJob,
  1008  					PodSpec: &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
  1009  					Refs:    &prowapi.Refs{Org: "fejtaverse"},
  1010  				},
  1011  				Status: prowapi.ProwJobStatus{
  1012  					State:   prowapi.PendingState,
  1013  					PodName: "boop-42",
  1014  				},
  1015  			},
  1016  			Pods: []v1.Pod{
  1017  				{
  1018  					ObjectMeta: metav1.ObjectMeta{
  1019  						Name:      "boop-42",
  1020  						Namespace: "pods",
  1021  					},
  1022  					Status: v1.PodStatus{
  1023  						Phase:                 v1.PodSucceeded,
  1024  						InitContainerStatuses: []v1.ContainerStatus{{LastTerminationState: v1.ContainerState{Terminated: &v1.ContainerStateTerminated{}}}},
  1025  					},
  1026  				},
  1027  			},
  1028  			ExpectedComplete:   true,
  1029  			ExpectedState:      prowapi.ErrorState,
  1030  			ExpectedNumPods:    1,
  1031  			ExpectedCreatedPJs: 0,
  1032  			ExpectedURL:        "boop-42/success",
  1033  		},
  1034  		{
  1035  			Name: "failed pod",
  1036  			PJ: prowapi.ProwJob{
  1037  				ObjectMeta: metav1.ObjectMeta{
  1038  					Name:      "boop-42",
  1039  					Namespace: "prowjobs",
  1040  				},
  1041  				Spec: prowapi.ProwJobSpec{
  1042  					Type: prowapi.PresubmitJob,
  1043  					Refs: &prowapi.Refs{
  1044  						Org: "kubernetes", Repo: "kubernetes",
  1045  						BaseRef: "baseref", BaseSHA: "basesha",
  1046  						Pulls: []prowapi.Pull{{Number: 100, Author: "me", SHA: "sha"}},
  1047  					},
  1048  					PodSpec: &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
  1049  				},
  1050  				Status: prowapi.ProwJobStatus{
  1051  					State:   prowapi.PendingState,
  1052  					PodName: "boop-42",
  1053  				},
  1054  			},
  1055  			Pods: []v1.Pod{
  1056  				{
  1057  					ObjectMeta: metav1.ObjectMeta{
  1058  						Name:      "boop-42",
  1059  						Namespace: "pods",
  1060  					},
  1061  					Status: v1.PodStatus{
  1062  						Phase: v1.PodFailed,
  1063  					},
  1064  				},
  1065  			},
  1066  			ExpectedComplete: true,
  1067  			ExpectedState:    prowapi.FailureState,
  1068  			ExpectedNumPods:  1,
  1069  			ExpectedURL:      "boop-42/failure",
  1070  		},
  1071  		{
  1072  			Name: "delete evicted pod",
  1073  			PJ: prowapi.ProwJob{
  1074  				ObjectMeta: metav1.ObjectMeta{
  1075  					Name:      "boop-42",
  1076  					Namespace: "prowjobs",
  1077  				},
  1078  				Spec: prowapi.ProwJobSpec{
  1079  					PodSpec: &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
  1080  				},
  1081  				Status: prowapi.ProwJobStatus{
  1082  					State:   prowapi.PendingState,
  1083  					PodName: "boop-42",
  1084  				},
  1085  			},
  1086  			Pods: []v1.Pod{
  1087  				{
  1088  					ObjectMeta: metav1.ObjectMeta{
  1089  						Name:      "boop-42",
  1090  						Namespace: "pods",
  1091  					},
  1092  					Status: v1.PodStatus{
  1093  						Phase:  v1.PodFailed,
  1094  						Reason: Evicted,
  1095  					},
  1096  				},
  1097  			},
  1098  			ExpectedComplete: false,
  1099  			ExpectedState:    prowapi.PendingState,
  1100  			ExpectedNumPods:  0,
  1101  		},
  1102  		{
  1103  			Name: "delete evicted pod and remove its k8sreporter finalizer",
  1104  			PJ: prowapi.ProwJob{
  1105  				ObjectMeta: metav1.ObjectMeta{
  1106  					Name:      "boop-42",
  1107  					Namespace: "prowjobs",
  1108  				},
  1109  				Spec: prowapi.ProwJobSpec{
  1110  					PodSpec: &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
  1111  				},
  1112  				Status: prowapi.ProwJobStatus{
  1113  					State:   prowapi.PendingState,
  1114  					PodName: "boop-42",
  1115  				},
  1116  			},
  1117  			Pods: []v1.Pod{
  1118  				{
  1119  					ObjectMeta: metav1.ObjectMeta{
  1120  						Name:       "boop-42",
  1121  						Namespace:  "pods",
  1122  						Finalizers: []string{"prow.x-k8s.io/gcsk8sreporter"},
  1123  					},
  1124  					Status: v1.PodStatus{
  1125  						Phase:  v1.PodFailed,
  1126  						Reason: Evicted,
  1127  					},
  1128  				},
  1129  			},
  1130  			ExpectedComplete: false,
  1131  			ExpectedState:    prowapi.PendingState,
  1132  			ExpectedNumPods:  0,
  1133  		},
  1134  		{
  1135  			Name: "don't delete evicted pod w/ error_on_eviction, complete PJ instead",
  1136  			PJ: prowapi.ProwJob{
  1137  				ObjectMeta: metav1.ObjectMeta{
  1138  					Name:      "boop-42",
  1139  					Namespace: "prowjobs",
  1140  				},
  1141  				Spec: prowapi.ProwJobSpec{
  1142  					ErrorOnEviction: true,
  1143  					PodSpec:         &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
  1144  				},
  1145  				Status: prowapi.ProwJobStatus{
  1146  					State:   prowapi.PendingState,
  1147  					PodName: "boop-42",
  1148  				},
  1149  			},
  1150  			Pods: []v1.Pod{
  1151  				{
  1152  					ObjectMeta: metav1.ObjectMeta{
  1153  						Name:      "boop-42",
  1154  						Namespace: "pods",
  1155  					},
  1156  					Status: v1.PodStatus{
  1157  						Phase:  v1.PodFailed,
  1158  						Reason: Evicted,
  1159  					},
  1160  				},
  1161  			},
  1162  			ExpectedComplete: true,
  1163  			ExpectedState:    prowapi.ErrorState,
  1164  			ExpectedNumPods:  1,
  1165  			ExpectedURL:      "boop-42/error",
  1166  		},
  1167  		{
  1168  			Name: "running pod",
  1169  			PJ: prowapi.ProwJob{
  1170  				ObjectMeta: metav1.ObjectMeta{
  1171  					Name:      "boop-42",
  1172  					Namespace: "prowjobs",
  1173  				},
  1174  				Spec: prowapi.ProwJobSpec{},
  1175  				Status: prowapi.ProwJobStatus{
  1176  					State:   prowapi.PendingState,
  1177  					PodName: "boop-42",
  1178  				},
  1179  			},
  1180  			Pods: []v1.Pod{
  1181  				{
  1182  					ObjectMeta: metav1.ObjectMeta{
  1183  						Name:      "boop-42",
  1184  						Namespace: "pods",
  1185  					},
  1186  					Status: v1.PodStatus{
  1187  						Phase: v1.PodRunning,
  1188  					},
  1189  				},
  1190  			},
  1191  			ExpectedState:   prowapi.PendingState,
  1192  			ExpectedNumPods: 1,
  1193  		},
  1194  		{
  1195  			Name: "pod changes url status",
  1196  			PJ: prowapi.ProwJob{
  1197  				ObjectMeta: metav1.ObjectMeta{
  1198  					Name:      "boop-42",
  1199  					Namespace: "prowjobs",
  1200  				},
  1201  				Spec: prowapi.ProwJobSpec{},
  1202  				Status: prowapi.ProwJobStatus{
  1203  					State:   prowapi.PendingState,
  1204  					PodName: "boop-42",
  1205  					URL:     "boop-42/pending",
  1206  				},
  1207  			},
  1208  			Pods: []v1.Pod{
  1209  				{
  1210  					ObjectMeta: metav1.ObjectMeta{
  1211  						Name:      "boop-42",
  1212  						Namespace: "pods",
  1213  					},
  1214  					Status: v1.PodStatus{
  1215  						Phase: v1.PodSucceeded,
  1216  					},
  1217  				},
  1218  			},
  1219  			ExpectedComplete:   true,
  1220  			ExpectedState:      prowapi.SuccessState,
  1221  			ExpectedNumPods:    1,
  1222  			ExpectedCreatedPJs: 0,
  1223  			ExpectedURL:        "boop-42/success",
  1224  		},
  1225  		{
  1226  			Name: "unprocessable prow job",
  1227  			PJ: prowapi.ProwJob{
  1228  				ObjectMeta: metav1.ObjectMeta{
  1229  					Name:      "jose",
  1230  					Namespace: "prowjobs",
  1231  				},
  1232  				Spec: prowapi.ProwJobSpec{
  1233  					Job:     "boop",
  1234  					Type:    prowapi.PostsubmitJob,
  1235  					PodSpec: &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
  1236  					Refs:    &prowapi.Refs{Org: "fejtaverse"},
  1237  				},
  1238  				Status: prowapi.ProwJobStatus{
  1239  					State: prowapi.PendingState,
  1240  				},
  1241  			},
  1242  			Err: &kapierrors.StatusError{ErrStatus: metav1.Status{
  1243  				Status: metav1.StatusFailure,
  1244  				Code:   http.StatusUnprocessableEntity,
  1245  				Reason: metav1.StatusReasonInvalid,
  1246  			}},
  1247  			ExpectedState:    prowapi.ErrorState,
  1248  			ExpectedComplete: true,
  1249  			ExpectedURL:      "jose/error",
  1250  		},
  1251  		{
  1252  			Name: "stale pending prow job",
  1253  			PJ: prowapi.ProwJob{
  1254  				ObjectMeta: metav1.ObjectMeta{
  1255  					Name:      "nightmare",
  1256  					Namespace: "prowjobs",
  1257  				},
  1258  				Spec: prowapi.ProwJobSpec{},
  1259  				Status: prowapi.ProwJobStatus{
  1260  					State:   prowapi.PendingState,
  1261  					PodName: "nightmare",
  1262  				},
  1263  			},
  1264  			Pods: []v1.Pod{
  1265  				{
  1266  					ObjectMeta: metav1.ObjectMeta{
  1267  						Name:              "nightmare",
  1268  						Namespace:         "pods",
  1269  						CreationTimestamp: metav1.Time{Time: time.Now().Add(-podPendingTimeout)},
  1270  					},
  1271  					Status: v1.PodStatus{
  1272  						Phase:     v1.PodPending,
  1273  						StartTime: startTime(time.Now().Add(-podPendingTimeout)),
  1274  					},
  1275  				},
  1276  			},
  1277  			ExpectedState:    prowapi.ErrorState,
  1278  			ExpectedNumPods:  0,
  1279  			ExpectedComplete: true,
  1280  			ExpectedURL:      "nightmare/error",
  1281  		},
  1282  		{
  1283  			Name: "stale pending prow job with specific podPendingTimeout",
  1284  			PJ: prowapi.ProwJob{
  1285  				ObjectMeta: metav1.ObjectMeta{
  1286  					Name:      "nightmare",
  1287  					Namespace: "prowjobs",
  1288  				},
  1289  				Spec: prowapi.ProwJobSpec{
  1290  					DecorationConfig: &prowapi.DecorationConfig{
  1291  						PodPendingTimeout: &metav1.Duration{Duration: 2 * time.Hour},
  1292  					},
  1293  				},
  1294  				Status: prowapi.ProwJobStatus{
  1295  					State:   prowapi.PendingState,
  1296  					PodName: "nightmare",
  1297  				},
  1298  			},
  1299  			Pods: []v1.Pod{
  1300  				{
  1301  					ObjectMeta: metav1.ObjectMeta{
  1302  						Name:              "nightmare",
  1303  						Namespace:         "pods",
  1304  						CreationTimestamp: metav1.Time{Time: time.Now().Add(-time.Hour * 2)},
  1305  					},
  1306  					Status: v1.PodStatus{
  1307  						Phase:     v1.PodPending,
  1308  						StartTime: startTime(time.Now().Add(-time.Hour * 2)),
  1309  					},
  1310  				},
  1311  			},
  1312  			ExpectedState:             prowapi.ErrorState,
  1313  			ExpectedNumPods:           0,
  1314  			ExpectedComplete:          true,
  1315  			ExpectedURL:               "nightmare/error",
  1316  			ExpectedPodPendingTimeout: &metav1.Duration{Duration: 2 * time.Hour},
  1317  		},
  1318  		{
  1319  			Name: "stale running prow job",
  1320  			PJ: prowapi.ProwJob{
  1321  				ObjectMeta: metav1.ObjectMeta{
  1322  					Name:      "endless",
  1323  					Namespace: "prowjobs",
  1324  				},
  1325  				Spec: prowapi.ProwJobSpec{},
  1326  				Status: prowapi.ProwJobStatus{
  1327  					State:   prowapi.PendingState,
  1328  					PodName: "endless",
  1329  				},
  1330  			},
  1331  			Pods: []v1.Pod{
  1332  				{
  1333  					ObjectMeta: metav1.ObjectMeta{
  1334  						Name:              "endless",
  1335  						Namespace:         "pods",
  1336  						CreationTimestamp: metav1.Time{Time: time.Now().Add(-podRunningTimeout)},
  1337  					},
  1338  					Status: v1.PodStatus{
  1339  						Phase:     v1.PodRunning,
  1340  						StartTime: startTime(time.Now().Add(-podRunningTimeout)),
  1341  					},
  1342  				},
  1343  			},
  1344  			ExpectedState:    prowapi.AbortedState,
  1345  			ExpectedNumPods:  0,
  1346  			ExpectedComplete: true,
  1347  			ExpectedURL:      "endless/aborted",
  1348  		},
  1349  		{
  1350  			Name: "stale running prow job with specific podRunningTimeout",
  1351  			PJ: prowapi.ProwJob{
  1352  				ObjectMeta: metav1.ObjectMeta{
  1353  					Name:      "endless",
  1354  					Namespace: "prowjobs",
  1355  				},
  1356  				Spec: prowapi.ProwJobSpec{
  1357  					DecorationConfig: &prowapi.DecorationConfig{
  1358  						PodRunningTimeout: &metav1.Duration{Duration: 1 * time.Hour},
  1359  					},
  1360  				},
  1361  				Status: prowapi.ProwJobStatus{
  1362  					State:   prowapi.PendingState,
  1363  					PodName: "endless",
  1364  				},
  1365  			},
  1366  			Pods: []v1.Pod{
  1367  				{
  1368  					ObjectMeta: metav1.ObjectMeta{
  1369  						Name:              "endless",
  1370  						Namespace:         "pods",
  1371  						CreationTimestamp: metav1.Time{Time: time.Now().Add(-time.Hour)},
  1372  					},
  1373  					Status: v1.PodStatus{
  1374  						Phase:     v1.PodRunning,
  1375  						StartTime: startTime(time.Now().Add(-time.Hour)),
  1376  					},
  1377  				},
  1378  			},
  1379  			ExpectedState:             prowapi.AbortedState,
  1380  			ExpectedNumPods:           0,
  1381  			ExpectedComplete:          true,
  1382  			ExpectedURL:               "endless/aborted",
  1383  			ExpectedPodRunningTimeout: &metav1.Duration{Duration: 1 * time.Hour},
  1384  		},
  1385  		{
  1386  			Name: "stale unschedulable prow job",
  1387  			PJ: prowapi.ProwJob{
  1388  				ObjectMeta: metav1.ObjectMeta{
  1389  					Name:      "homeless",
  1390  					Namespace: "prowjobs",
  1391  				},
  1392  				Spec: prowapi.ProwJobSpec{},
  1393  				Status: prowapi.ProwJobStatus{
  1394  					State:   prowapi.PendingState,
  1395  					PodName: "homeless",
  1396  				},
  1397  			},
  1398  			Pods: []v1.Pod{
  1399  				{
  1400  					ObjectMeta: metav1.ObjectMeta{
  1401  						Name:              "homeless",
  1402  						Namespace:         "pods",
  1403  						CreationTimestamp: metav1.Time{Time: time.Now().Add(-podUnscheduledTimeout - time.Second)},
  1404  					},
  1405  					Status: v1.PodStatus{
  1406  						Phase: v1.PodPending,
  1407  					},
  1408  				},
  1409  			},
  1410  			ExpectedState:    prowapi.ErrorState,
  1411  			ExpectedNumPods:  0,
  1412  			ExpectedComplete: true,
  1413  			ExpectedURL:      "homeless/error",
  1414  		},
  1415  		{
  1416  			Name: "stale unschedulable prow job with specific podUnscheduledTimeout",
  1417  			PJ: prowapi.ProwJob{
  1418  				ObjectMeta: metav1.ObjectMeta{
  1419  					Name:      "homeless",
  1420  					Namespace: "prowjobs",
  1421  				},
  1422  				Spec: prowapi.ProwJobSpec{
  1423  					DecorationConfig: &prowapi.DecorationConfig{
  1424  						PodUnscheduledTimeout: &metav1.Duration{Duration: 2 * time.Minute},
  1425  					},
  1426  				},
  1427  				Status: prowapi.ProwJobStatus{
  1428  					State:   prowapi.PendingState,
  1429  					PodName: "homeless",
  1430  				},
  1431  			},
  1432  			Pods: []v1.Pod{
  1433  				{
  1434  					ObjectMeta: metav1.ObjectMeta{
  1435  						Name:              "homeless",
  1436  						Namespace:         "pods",
  1437  						CreationTimestamp: metav1.Time{Time: time.Now().Add(-2*time.Minute - time.Second)},
  1438  					},
  1439  					Status: v1.PodStatus{
  1440  						Phase: v1.PodPending,
  1441  					},
  1442  				},
  1443  			},
  1444  			ExpectedState:                 prowapi.ErrorState,
  1445  			ExpectedNumPods:               0,
  1446  			ExpectedComplete:              true,
  1447  			ExpectedURL:                   "homeless/error",
  1448  			ExpectedPodUnscheduledTimeout: &metav1.Duration{Duration: 2 * time.Minute},
  1449  		},
  1450  		{
  1451  			Name: "pending, created less than podPendingTimeout ago",
  1452  			PJ: prowapi.ProwJob{
  1453  				ObjectMeta: metav1.ObjectMeta{
  1454  					Name:      "slowpoke",
  1455  					Namespace: "prowjobs",
  1456  				},
  1457  				Spec: prowapi.ProwJobSpec{},
  1458  				Status: prowapi.ProwJobStatus{
  1459  					State:   prowapi.PendingState,
  1460  					PodName: "slowpoke",
  1461  				},
  1462  			},
  1463  			Pods: []v1.Pod{
  1464  				{
  1465  					ObjectMeta: metav1.ObjectMeta{
  1466  						Name:              "slowpoke",
  1467  						Namespace:         "pods",
  1468  						CreationTimestamp: metav1.Time{Time: time.Now().Add(-(podPendingTimeout - 10*time.Minute))},
  1469  					},
  1470  					Status: v1.PodStatus{
  1471  						Phase:     v1.PodPending,
  1472  						StartTime: startTime(time.Now().Add(-(podPendingTimeout - 10*time.Minute))),
  1473  					},
  1474  				},
  1475  			},
  1476  			expectedReconcileResult: &reconcile.Result{RequeueAfter: 10 * time.Minute},
  1477  			ExpectedState:           prowapi.PendingState,
  1478  			ExpectedNumPods:         1,
  1479  		},
  1480  		{
  1481  			Name: "unscheduled, created less than podUnscheduledTimeout ago",
  1482  			PJ: prowapi.ProwJob{
  1483  				ObjectMeta: metav1.ObjectMeta{
  1484  					Name:      "just-waiting",
  1485  					Namespace: "prowjobs",
  1486  				},
  1487  				Spec: prowapi.ProwJobSpec{},
  1488  				Status: prowapi.ProwJobStatus{
  1489  					State:   prowapi.PendingState,
  1490  					PodName: "just-waiting",
  1491  				},
  1492  			},
  1493  			Pods: []v1.Pod{
  1494  				{
  1495  					ObjectMeta: metav1.ObjectMeta{
  1496  						Name:              "just-waiting",
  1497  						Namespace:         "pods",
  1498  						CreationTimestamp: metav1.Time{Time: time.Now().Add(-time.Second)},
  1499  					},
  1500  					Status: v1.PodStatus{
  1501  						Phase: v1.PodPending,
  1502  					},
  1503  				},
  1504  			},
  1505  			expectedReconcileResult: &reconcile.Result{RequeueAfter: podUnscheduledTimeout},
  1506  			ExpectedState:           prowapi.PendingState,
  1507  			ExpectedNumPods:         1,
  1508  		},
  1509  		{
  1510  			Name: "Pod deleted in pending phase, job marked as errored",
  1511  			PJ: prowapi.ProwJob{
  1512  				ObjectMeta: metav1.ObjectMeta{
  1513  					Name:      "deleted-pod-in-pending-marks-job-as-errored",
  1514  					Namespace: "prowjobs",
  1515  				},
  1516  				Spec: prowapi.ProwJobSpec{},
  1517  				Status: prowapi.ProwJobStatus{
  1518  					State:   prowapi.PendingState,
  1519  					PodName: "deleted-pod-in-pending-marks-job-as-errored",
  1520  				},
  1521  			},
  1522  			Pods: []v1.Pod{
  1523  				{
  1524  					ObjectMeta: metav1.ObjectMeta{
  1525  						Name:              "deleted-pod-in-pending-marks-job-as-errored",
  1526  						Namespace:         "pods",
  1527  						CreationTimestamp: metav1.Time{Time: time.Now().Add(-time.Second)},
  1528  						DeletionTimestamp: func() *metav1.Time { n := metav1.Now(); return &n }(),
  1529  					},
  1530  					Status: v1.PodStatus{
  1531  						Phase: v1.PodPending,
  1532  					},
  1533  				},
  1534  			},
  1535  			ExpectedState:    prowapi.ErrorState,
  1536  			ExpectedComplete: true,
  1537  			ExpectedNumPods:  1,
  1538  		},
  1539  		{
  1540  			Name: "Pod deleted in unset phase, job marked as errored",
  1541  			PJ: prowapi.ProwJob{
  1542  				ObjectMeta: metav1.ObjectMeta{
  1543  					Name:      "pod-deleted-in-unset-phase",
  1544  					Namespace: "prowjobs",
  1545  				},
  1546  				Spec: prowapi.ProwJobSpec{},
  1547  				Status: prowapi.ProwJobStatus{
  1548  					State:   prowapi.PendingState,
  1549  					PodName: "pod-deleted-in-unset-phase",
  1550  				},
  1551  			},
  1552  			Pods: []v1.Pod{
  1553  				{
  1554  					ObjectMeta: metav1.ObjectMeta{
  1555  						Name:              "pod-deleted-in-unset-phase",
  1556  						Namespace:         "pods",
  1557  						CreationTimestamp: metav1.Time{Time: time.Now().Add(-time.Second)},
  1558  						DeletionTimestamp: func() *metav1.Time { n := metav1.Now(); return &n }(),
  1559  					},
  1560  				},
  1561  			},
  1562  			ExpectedState:    prowapi.ErrorState,
  1563  			ExpectedComplete: true,
  1564  			ExpectedNumPods:  1,
  1565  		},
  1566  		{
  1567  			Name: "Pod deleted in running phase, job marked as errored",
  1568  			PJ: prowapi.ProwJob{
  1569  				ObjectMeta: metav1.ObjectMeta{
  1570  					Name:      "pod-deleted-in-unset-phase",
  1571  					Namespace: "prowjobs",
  1572  				},
  1573  				Spec: prowapi.ProwJobSpec{},
  1574  				Status: prowapi.ProwJobStatus{
  1575  					State:   prowapi.PendingState,
  1576  					PodName: "pod-deleted-in-unset-phase",
  1577  				},
  1578  			},
  1579  			Pods: []v1.Pod{
  1580  				{
  1581  					ObjectMeta: metav1.ObjectMeta{
  1582  						Name:              "pod-deleted-in-unset-phase",
  1583  						Namespace:         "pods",
  1584  						CreationTimestamp: metav1.Time{Time: time.Now().Add(-time.Second)},
  1585  						DeletionTimestamp: func() *metav1.Time { n := metav1.Now(); return &n }(),
  1586  					},
  1587  					Status: v1.PodStatus{
  1588  						Phase: v1.PodRunning,
  1589  					},
  1590  				},
  1591  			},
  1592  			ExpectedState:    prowapi.ErrorState,
  1593  			ExpectedComplete: true,
  1594  			ExpectedNumPods:  1,
  1595  		},
  1596  		{
  1597  			Name: "Pod deleted with NodeLost reason in running phase, pod finalizer gets cleaned up",
  1598  			PJ: prowapi.ProwJob{
  1599  				ObjectMeta: metav1.ObjectMeta{
  1600  					Name:      "pod-deleted-in-running-phase",
  1601  					Namespace: "prowjobs",
  1602  				},
  1603  				Spec: prowapi.ProwJobSpec{},
  1604  				Status: prowapi.ProwJobStatus{
  1605  					State:   prowapi.PendingState,
  1606  					PodName: "pod-deleted-in-running-phase",
  1607  				},
  1608  			},
  1609  			Pods: []v1.Pod{
  1610  				{
  1611  					ObjectMeta: metav1.ObjectMeta{
  1612  						Name:              "pod-deleted-in-running-phase",
  1613  						Namespace:         "pods",
  1614  						CreationTimestamp: metav1.Time{Time: time.Now().Add(-time.Second)},
  1615  						DeletionTimestamp: func() *metav1.Time { n := metav1.Now(); return &n }(),
  1616  						Finalizers:        []string{"prow.x-k8s.io/gcsk8sreporter"},
  1617  					},
  1618  					Status: v1.PodStatus{
  1619  						Phase:  v1.PodRunning,
  1620  						Reason: "NodeLost",
  1621  					},
  1622  				},
  1623  			},
  1624  			ExpectedState:    prowapi.PendingState,
  1625  			ExpectedComplete: false,
  1626  			ExpectedNumPods:  0,
  1627  		},
  1628  	}
  1629  
  1630  	for _, tc := range testcases {
  1631  		t.Run(tc.Name, func(t *testing.T) {
  1632  			totServ := httptest.NewServer(http.HandlerFunc(handleTot))
  1633  			defer totServ.Close()
  1634  			pm := make(map[string]v1.Pod)
  1635  			for i := range tc.Pods {
  1636  				pm[tc.Pods[i].ObjectMeta.Name] = tc.Pods[i]
  1637  			}
  1638  			fakeProwJobClient := fakectrlruntimeclient.NewClientBuilder().WithRuntimeObjects(&tc.PJ).Build()
  1639  
  1640  			builder := fakectrlruntimeclient.NewClientBuilder()
  1641  			for i := range tc.Pods {
  1642  				builder.WithRuntimeObjects(&tc.Pods[i])
  1643  			}
  1644  
  1645  			fakeClient := &clientWrapper{
  1646  				Client:                   builder.Build(),
  1647  				createError:              tc.Err,
  1648  				errOnDeleteWithFinalizer: true,
  1649  			}
  1650  			buildClients := map[string]buildClient{
  1651  				prowapi.DefaultClusterAlias: {
  1652  					Client: fakeClient,
  1653  				},
  1654  			}
  1655  
  1656  			r := &reconciler{
  1657  				pjClient:     fakeProwJobClient,
  1658  				buildClients: buildClients,
  1659  				log:          logrus.NewEntry(logrus.StandardLogger()),
  1660  				config:       newFakeConfigAgent(t, 0, nil).Config,
  1661  				totURL:       totServ.URL,
  1662  				clock:        clock.RealClock{},
  1663  			}
  1664  			reconcileResult, err := r.syncPendingJob(context.Background(), &tc.PJ)
  1665  			if err != nil {
  1666  				t.Fatalf("syncPendingJob failed: %v", err)
  1667  			}
  1668  			if reconcileResult != nil {
  1669  				// Round this to minutes so we can compare the value without risking flaky tests
  1670  				reconcileResult.RequeueAfter = reconcileResult.RequeueAfter.Round(time.Minute)
  1671  			}
  1672  			if diff := cmp.Diff(tc.expectedReconcileResult, reconcileResult); diff != "" {
  1673  				t.Errorf("expected reconcileResult differs from actual: %s", diff)
  1674  			}
  1675  
  1676  			actualProwJobs := &prowapi.ProwJobList{}
  1677  			if err := fakeProwJobClient.List(context.Background(), actualProwJobs); err != nil {
  1678  				t.Errorf("could not list prowJobs from the client: %v", err)
  1679  			}
  1680  			if len(actualProwJobs.Items) != tc.ExpectedCreatedPJs+1 {
  1681  				t.Errorf("got %d created prowjobs", len(actualProwJobs.Items)-1)
  1682  			}
  1683  			actual := actualProwJobs.Items[0]
  1684  			if actual.Status.State != tc.ExpectedState {
  1685  				t.Errorf("got state %v", actual.Status.State)
  1686  			}
  1687  			if tc.ExpectedBuildID != "" && actual.Status.BuildID != tc.ExpectedBuildID {
  1688  				t.Errorf("expected BuildID %q, got %q", tc.ExpectedBuildID, actual.Status.BuildID)
  1689  			}
  1690  			if actual.Spec.DecorationConfig != nil && actual.Spec.DecorationConfig.PodRunningTimeout != nil &&
  1691  				tc.ExpectedPodRunningTimeout.Duration != actual.Spec.DecorationConfig.PodRunningTimeout.Duration {
  1692  				t.Errorf("expected PodRunningTimeout %v, got %v",
  1693  					tc.ExpectedPodRunningTimeout.Duration, actual.Spec.DecorationConfig.PodRunningTimeout.Duration)
  1694  			}
  1695  			if actual.Spec.DecorationConfig != nil && actual.Spec.DecorationConfig.PodPendingTimeout != nil &&
  1696  				tc.ExpectedPodPendingTimeout.Duration != actual.Spec.DecorationConfig.PodPendingTimeout.Duration {
  1697  				t.Errorf("expected PodPendingTimeout %v, got %v",
  1698  					tc.ExpectedPodPendingTimeout.Duration, actual.Spec.DecorationConfig.PodPendingTimeout.Duration)
  1699  			}
  1700  			if actual.Spec.DecorationConfig != nil && actual.Spec.DecorationConfig.PodUnscheduledTimeout != nil &&
  1701  				tc.ExpectedPodUnscheduledTimeout.Duration != actual.Spec.DecorationConfig.PodUnscheduledTimeout.Duration {
  1702  				t.Errorf("expected PodUnscheduledTimeout %v, got %v",
  1703  					tc.ExpectedPodUnscheduledTimeout.Duration, actual.Spec.DecorationConfig.PodUnscheduledTimeout.Duration)
  1704  			}
  1705  			actualPods := &v1.PodList{}
  1706  			if err := buildClients[prowapi.DefaultClusterAlias].List(context.Background(), actualPods); err != nil {
  1707  				t.Errorf("could not list pods from the client: %v", err)
  1708  			}
  1709  			if got := len(actualPods.Items); got != tc.ExpectedNumPods {
  1710  				t.Errorf("got %d pods, expected %d", len(actualPods.Items), tc.ExpectedNumPods)
  1711  			}
  1712  			for _, pod := range actualPods.Items {
  1713  				if pod.DeletionTimestamp != nil && len(pod.Finalizers) != 0 {
  1714  					t.Errorf("pod %s was deleted but still had finalizers: %v", pod.Name, pod.Finalizers)
  1715  				}
  1716  			}
  1717  			if actual := actual.Complete(); actual != tc.ExpectedComplete {
  1718  				t.Errorf("expected complete: %t, got complete: %t", tc.ExpectedComplete, actual)
  1719  			}
  1720  		})
  1721  	}
  1722  }
  1723  
  1724  // TestPeriodic walks through the happy path of a periodic job.
  1725  func TestPeriodic(t *testing.T) {
  1726  	per := config.Periodic{
  1727  		JobBase: config.JobBase{
  1728  			Name:    "ci-periodic-job",
  1729  			Agent:   "kubernetes",
  1730  			Cluster: "trusted",
  1731  			Spec:    &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
  1732  		},
  1733  	}
  1734  
  1735  	totServ := httptest.NewServer(http.HandlerFunc(handleTot))
  1736  	defer totServ.Close()
  1737  	pj := pjutil.NewProwJob(pjutil.PeriodicSpec(per), nil, nil)
  1738  	pj.Namespace = "prowjobs"
  1739  	fakeProwJobClient := fakectrlruntimeclient.NewClientBuilder().WithRuntimeObjects(&pj).Build()
  1740  	buildClients := map[string]buildClient{
  1741  		prowapi.DefaultClusterAlias: {
  1742  			Client: fakectrlruntimeclient.NewClientBuilder().Build(),
  1743  		},
  1744  		"trusted": {
  1745  			Client: fakectrlruntimeclient.NewClientBuilder().Build(),
  1746  		},
  1747  	}
  1748  
  1749  	logger := logrus.New()
  1750  	logger.SetLevel(logrus.DebugLevel)
  1751  	log := logrus.NewEntry(logger)
  1752  	r := reconciler{
  1753  		pjClient:     fakeProwJobClient,
  1754  		buildClients: buildClients,
  1755  		log:          log,
  1756  		config:       newFakeConfigAgent(t, 0, nil).Config,
  1757  		totURL:       totServ.URL,
  1758  		clock:        clock.RealClock{},
  1759  	}
  1760  	if _, err := r.Reconcile(context.Background(), reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "prowjobs", Name: pj.Name}}); err != nil {
  1761  		t.Fatalf("Error on first sync: %v", err)
  1762  	}
  1763  
  1764  	afterFirstSync := &prowapi.ProwJobList{}
  1765  	if err := fakeProwJobClient.List(context.Background(), afterFirstSync); err != nil {
  1766  		t.Fatalf("could not list prowJobs from the client: %v", err)
  1767  	}
  1768  	if len(afterFirstSync.Items) != 1 {
  1769  		t.Fatalf("saw %d prowjobs after sync, not 1", len(afterFirstSync.Items))
  1770  	}
  1771  	if len(afterFirstSync.Items[0].Spec.PodSpec.Containers) != 1 || afterFirstSync.Items[0].Spec.PodSpec.Containers[0].Name != "test-name" {
  1772  		t.Fatalf("Sync step updated the pod spec: %#v", afterFirstSync.Items[0].Spec.PodSpec)
  1773  	}
  1774  	podsAfterSync := &v1.PodList{}
  1775  	if err := buildClients["trusted"].List(context.Background(), podsAfterSync); err != nil {
  1776  		t.Fatalf("could not list pods from the client: %v", err)
  1777  	}
  1778  	if len(podsAfterSync.Items) != 1 {
  1779  		t.Fatalf("expected exactly one pod, got %d", len(podsAfterSync.Items))
  1780  	}
  1781  	if len(podsAfterSync.Items[0].Spec.Containers) != 1 {
  1782  		t.Fatal("Wiped container list.")
  1783  	}
  1784  	if len(podsAfterSync.Items[0].Spec.Containers[0].Env) == 0 {
  1785  		t.Fatal("Container has no env set.")
  1786  	}
  1787  	if _, err := r.Reconcile(context.Background(), reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "prowjobs", Name: pj.Name}}); err != nil {
  1788  		t.Fatalf("Error on second sync: %v", err)
  1789  	}
  1790  	podsAfterSecondSync := &v1.PodList{}
  1791  	if err := buildClients["trusted"].List(context.Background(), podsAfterSecondSync); err != nil {
  1792  		t.Fatalf("could not list pods from the client: %v", err)
  1793  	}
  1794  	if len(podsAfterSecondSync.Items) != 1 {
  1795  		t.Fatalf("Wrong number of pods after second sync: %d", len(podsAfterSecondSync.Items))
  1796  	}
  1797  	update := podsAfterSecondSync.Items[0].DeepCopy()
  1798  	update.Status.Phase = v1.PodSucceeded
  1799  	if err := buildClients["trusted"].Update(context.Background(), update); err != nil {
  1800  		t.Fatalf("could not update pod to be succeeded: %v", err)
  1801  	}
  1802  	if _, err := r.Reconcile(context.Background(), reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "prowjobs", Name: pj.Name}}); err != nil {
  1803  		t.Fatalf("Error on third sync: %v", err)
  1804  	}
  1805  	afterThirdSync := &prowapi.ProwJobList{}
  1806  	if err := fakeProwJobClient.List(context.Background(), afterThirdSync); err != nil {
  1807  		t.Fatalf("could not list prowJobs from the client: %v", err)
  1808  	}
  1809  	if len(afterThirdSync.Items) != 1 {
  1810  		t.Fatalf("Wrong number of prow jobs: %d", len(afterThirdSync.Items))
  1811  	}
  1812  	if !afterThirdSync.Items[0].Complete() {
  1813  		t.Fatal("Prow job didn't complete.")
  1814  	}
  1815  	if afterThirdSync.Items[0].Status.State != prowapi.SuccessState {
  1816  		t.Fatalf("Should be success: %v", afterThirdSync.Items[0].Status.State)
  1817  	}
  1818  	if _, err := r.Reconcile(context.Background(), reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "prowjobs", Name: pj.Name}}); err != nil {
  1819  		t.Fatalf("Error on fourth sync: %v", err)
  1820  	}
  1821  }
  1822  
  1823  func TestMaxConcurrencyWithNewlyTriggeredJobs(t *testing.T) {
  1824  	type testCase struct {
  1825  		Name         string
  1826  		PJs          []prowapi.ProwJob
  1827  		PendingJobs  map[string]int
  1828  		ExpectedPods int
  1829  	}
  1830  
  1831  	tests := []testCase{
  1832  		{
  1833  			Name: "avoid starting a triggered job",
  1834  			PJs: []prowapi.ProwJob{
  1835  				{
  1836  					ObjectMeta: metav1.ObjectMeta{
  1837  						Name: "first",
  1838  					},
  1839  					Spec: prowapi.ProwJobSpec{
  1840  						Job:            "test-bazel-build",
  1841  						Type:           prowapi.PostsubmitJob,
  1842  						MaxConcurrency: 1,
  1843  						PodSpec:        &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
  1844  						Refs:           &prowapi.Refs{Org: "fejtaverse"},
  1845  					},
  1846  					Status: prowapi.ProwJobStatus{
  1847  						State: prowapi.TriggeredState,
  1848  					},
  1849  				},
  1850  				{
  1851  					ObjectMeta: metav1.ObjectMeta{
  1852  						Name:              "second",
  1853  						CreationTimestamp: metav1.Now(),
  1854  					},
  1855  					Spec: prowapi.ProwJobSpec{
  1856  						Job:            "test-bazel-build",
  1857  						Type:           prowapi.PostsubmitJob,
  1858  						MaxConcurrency: 1,
  1859  						PodSpec:        &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
  1860  						Refs:           &prowapi.Refs{Org: "fejtaverse"},
  1861  					},
  1862  					Status: prowapi.ProwJobStatus{
  1863  						State: prowapi.TriggeredState,
  1864  					},
  1865  				},
  1866  			},
  1867  			PendingJobs:  make(map[string]int),
  1868  			ExpectedPods: 1,
  1869  		},
  1870  		{
  1871  			Name: "both triggered jobs can start",
  1872  			PJs: []prowapi.ProwJob{
  1873  				{
  1874  					ObjectMeta: metav1.ObjectMeta{
  1875  						Name: "first",
  1876  					},
  1877  					Spec: prowapi.ProwJobSpec{
  1878  						Job:            "test-bazel-build",
  1879  						Type:           prowapi.PostsubmitJob,
  1880  						MaxConcurrency: 2,
  1881  						PodSpec:        &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
  1882  						Refs:           &prowapi.Refs{Org: "fejtaverse"},
  1883  					},
  1884  					Status: prowapi.ProwJobStatus{
  1885  						State: prowapi.TriggeredState,
  1886  					},
  1887  				},
  1888  				{
  1889  					ObjectMeta: metav1.ObjectMeta{
  1890  						Name: "second",
  1891  					},
  1892  					Spec: prowapi.ProwJobSpec{
  1893  						Job:            "test-bazel-build",
  1894  						Type:           prowapi.PostsubmitJob,
  1895  						MaxConcurrency: 2,
  1896  						PodSpec:        &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
  1897  						Refs:           &prowapi.Refs{Org: "fejtaverse"},
  1898  					},
  1899  					Status: prowapi.ProwJobStatus{
  1900  						State: prowapi.TriggeredState,
  1901  					},
  1902  				},
  1903  			},
  1904  			PendingJobs:  make(map[string]int),
  1905  			ExpectedPods: 2,
  1906  		},
  1907  		{
  1908  			Name: "no triggered job can start",
  1909  			PJs: []prowapi.ProwJob{
  1910  				{
  1911  					ObjectMeta: metav1.ObjectMeta{
  1912  						Name:              "first",
  1913  						CreationTimestamp: metav1.Now(),
  1914  					},
  1915  					Spec: prowapi.ProwJobSpec{
  1916  						Job:            "test-bazel-build",
  1917  						Type:           prowapi.PostsubmitJob,
  1918  						MaxConcurrency: 5,
  1919  						PodSpec:        &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
  1920  						Refs:           &prowapi.Refs{Org: "fejtaverse"},
  1921  					},
  1922  					Status: prowapi.ProwJobStatus{
  1923  						State: prowapi.TriggeredState,
  1924  					},
  1925  				},
  1926  				{
  1927  					ObjectMeta: metav1.ObjectMeta{
  1928  						Name:              "second",
  1929  						CreationTimestamp: metav1.Now(),
  1930  					},
  1931  					Spec: prowapi.ProwJobSpec{
  1932  						Job:            "test-bazel-build",
  1933  						Type:           prowapi.PostsubmitJob,
  1934  						MaxConcurrency: 5,
  1935  						PodSpec:        &v1.PodSpec{Containers: []v1.Container{{Name: "test-name", Env: []v1.EnvVar{}}}},
  1936  						Refs:           &prowapi.Refs{Org: "fejtaverse"},
  1937  					},
  1938  					Status: prowapi.ProwJobStatus{
  1939  						State: prowapi.TriggeredState,
  1940  					},
  1941  				},
  1942  			},
  1943  			PendingJobs:  map[string]int{"test-bazel-build": 5},
  1944  			ExpectedPods: 0,
  1945  		},
  1946  	}
  1947  
  1948  	for _, test := range tests {
  1949  		t.Run(test.Name, func(t *testing.T) {
  1950  			jobs := make(chan prowapi.ProwJob, len(test.PJs))
  1951  			for _, pj := range test.PJs {
  1952  				jobs <- pj
  1953  			}
  1954  			close(jobs)
  1955  
  1956  			builder := fakectrlruntimeclient.NewClientBuilder()
  1957  			for i := range test.PJs {
  1958  				test.PJs[i].Namespace = "prowjobs"
  1959  				test.PJs[i].Spec.Agent = prowapi.KubernetesAgent
  1960  				test.PJs[i].UID = types.UID(strconv.Itoa(i))
  1961  
  1962  				builder.WithRuntimeObjects(&test.PJs[i])
  1963  			}
  1964  
  1965  			fakeProwJobClient := builder.Build()
  1966  			buildClients := map[string]buildClient{
  1967  				prowapi.DefaultClusterAlias: {
  1968  					Client: fakectrlruntimeclient.NewClientBuilder().Build(),
  1969  				},
  1970  			}
  1971  
  1972  			for jobName, numJobsToCreate := range test.PendingJobs {
  1973  				for i := 0; i < numJobsToCreate; i++ {
  1974  					if err := fakeProwJobClient.Create(context.Background(), &prowapi.ProwJob{
  1975  						ObjectMeta: metav1.ObjectMeta{
  1976  							Name:      fmt.Sprintf("%s-%d", jobName, i),
  1977  							Namespace: "prowjobs",
  1978  						},
  1979  						Spec: prowapi.ProwJobSpec{
  1980  							Agent: prowapi.KubernetesAgent,
  1981  							Job:   jobName,
  1982  						},
  1983  						Status: prowapi.ProwJobStatus{
  1984  							State: prowapi.PendingState,
  1985  						},
  1986  					}); err != nil {
  1987  						t.Fatalf("failed to create prowJob: %v", err)
  1988  					}
  1989  				}
  1990  			}
  1991  			r := newReconciler(context.Background(),
  1992  				&indexingClient{
  1993  					Client:     fakeProwJobClient,
  1994  					indexFuncs: map[string]ctrlruntimeclient.IndexerFunc{prowJobIndexName: prowJobIndexer("prowjobs")},
  1995  				}, nil, newFakeConfigAgent(t, 0, nil).Config, nil, "")
  1996  			r.buildClients = buildClients
  1997  			for _, job := range test.PJs {
  1998  				request := reconcile.Request{NamespacedName: types.NamespacedName{
  1999  					Name:      job.Name,
  2000  					Namespace: job.Namespace,
  2001  				}}
  2002  				if _, err := r.Reconcile(context.Background(), request); err != nil {
  2003  					t.Fatalf("failed to reconcile job %s: %v", request.String(), err)
  2004  				}
  2005  			}
  2006  
  2007  			podsAfterSync := &v1.PodList{}
  2008  			if err := buildClients[prowapi.DefaultClusterAlias].List(context.Background(), podsAfterSync); err != nil {
  2009  				t.Fatalf("could not list pods from the client: %v", err)
  2010  			}
  2011  			if len(podsAfterSync.Items) != test.ExpectedPods {
  2012  				t.Errorf("expected pods: %d, got: %d", test.ExpectedPods, len(podsAfterSync.Items))
  2013  			}
  2014  		})
  2015  	}
  2016  }
  2017  
  2018  func TestMaxConcurency(t *testing.T) {
  2019  	type pendingJob struct {
  2020  		Duplicates int
  2021  		JobQueue   string
  2022  	}
  2023  
  2024  	type testCase struct {
  2025  		Name               string
  2026  		JobQueueCapacities map[string]int
  2027  		ProwJob            prowapi.ProwJob
  2028  		ExistingProwJobs   []prowapi.ProwJob
  2029  		PendingJobs        map[string]pendingJob
  2030  
  2031  		ExpectedResult bool
  2032  	}
  2033  	testCases := []testCase{
  2034  		{
  2035  			Name:           "Max concurency 0 always runs",
  2036  			ProwJob:        prowapi.ProwJob{Spec: prowapi.ProwJobSpec{MaxConcurrency: 0}},
  2037  			ExpectedResult: true,
  2038  		},
  2039  		{
  2040  			Name: "Num pending exceeds max concurrency",
  2041  			ProwJob: prowapi.ProwJob{
  2042  				ObjectMeta: metav1.ObjectMeta{CreationTimestamp: metav1.Now()},
  2043  				Spec: prowapi.ProwJobSpec{
  2044  					MaxConcurrency: 10,
  2045  					Job:            "my-pj",
  2046  				},
  2047  			},
  2048  			PendingJobs:    map[string]pendingJob{"my-pj": {Duplicates: 10}},
  2049  			ExpectedResult: false,
  2050  		},
  2051  		{
  2052  			Name: "Num pending plus older instances equals max concurency",
  2053  			ProwJob: prowapi.ProwJob{
  2054  				ObjectMeta: metav1.ObjectMeta{
  2055  					CreationTimestamp: metav1.Now(),
  2056  				},
  2057  				Spec: prowapi.ProwJobSpec{
  2058  					MaxConcurrency: 10,
  2059  					Job:            "my-pj",
  2060  				},
  2061  			},
  2062  			ExistingProwJobs: []prowapi.ProwJob{
  2063  				{
  2064  					ObjectMeta: metav1.ObjectMeta{Namespace: "prowjobs"},
  2065  					Spec:       prowapi.ProwJobSpec{Agent: prowapi.KubernetesAgent, Job: "my-pj"},
  2066  					Status: prowapi.ProwJobStatus{
  2067  						State: prowapi.TriggeredState,
  2068  					},
  2069  				},
  2070  			},
  2071  			PendingJobs:    map[string]pendingJob{"my-pj": {Duplicates: 9}},
  2072  			ExpectedResult: false,
  2073  		},
  2074  		{
  2075  			Name: "Num pending plus older instances exceeds max concurency",
  2076  			ProwJob: prowapi.ProwJob{
  2077  				ObjectMeta: metav1.ObjectMeta{
  2078  					CreationTimestamp: metav1.Now(),
  2079  				},
  2080  				Spec: prowapi.ProwJobSpec{
  2081  					MaxConcurrency: 10,
  2082  					Job:            "my-pj",
  2083  				},
  2084  			},
  2085  			ExistingProwJobs: []prowapi.ProwJob{
  2086  				{
  2087  					Spec: prowapi.ProwJobSpec{Job: "my-pj"},
  2088  					Status: prowapi.ProwJobStatus{
  2089  						State: prowapi.TriggeredState,
  2090  					},
  2091  				},
  2092  			},
  2093  			PendingJobs:    map[string]pendingJob{"my-pj": {Duplicates: 10}},
  2094  			ExpectedResult: false,
  2095  		},
  2096  		{
  2097  			Name: "Have other jobs that are newer, can execute",
  2098  			ProwJob: prowapi.ProwJob{
  2099  				Spec: prowapi.ProwJobSpec{
  2100  					MaxConcurrency: 1,
  2101  					Job:            "my-pj",
  2102  				},
  2103  			},
  2104  			ExistingProwJobs: []prowapi.ProwJob{
  2105  				{
  2106  					ObjectMeta: metav1.ObjectMeta{
  2107  						CreationTimestamp: metav1.Now(),
  2108  					},
  2109  					Spec: prowapi.ProwJobSpec{Job: "my-pj"},
  2110  					Status: prowapi.ProwJobStatus{
  2111  						State: prowapi.TriggeredState,
  2112  					},
  2113  				},
  2114  			},
  2115  			ExpectedResult: true,
  2116  		},
  2117  		{
  2118  			Name: "Have older jobs that are not triggered, can execute",
  2119  			ProwJob: prowapi.ProwJob{
  2120  				ObjectMeta: metav1.ObjectMeta{
  2121  					CreationTimestamp: metav1.Now(),
  2122  				},
  2123  				Spec: prowapi.ProwJobSpec{
  2124  					MaxConcurrency: 2,
  2125  					Job:            "my-pj",
  2126  				},
  2127  			},
  2128  			ExistingProwJobs: []prowapi.ProwJob{
  2129  				{
  2130  					Spec: prowapi.ProwJobSpec{Job: "my-pj"},
  2131  					Status: prowapi.ProwJobStatus{
  2132  						CompletionTime: &[]metav1.Time{{}}[0],
  2133  					},
  2134  				},
  2135  			},
  2136  			PendingJobs:    map[string]pendingJob{"my-pj": {Duplicates: 1}},
  2137  			ExpectedResult: true,
  2138  		},
  2139  		{
  2140  			Name:               "Job queue capacity 0 never runs",
  2141  			ProwJob:            prowapi.ProwJob{Spec: prowapi.ProwJobSpec{JobQueueName: "queue"}},
  2142  			JobQueueCapacities: map[string]int{"queue": 0},
  2143  			ExpectedResult:     false,
  2144  		},
  2145  		{
  2146  			Name:               "Job queue capacity -1 always runs",
  2147  			ProwJob:            prowapi.ProwJob{Spec: prowapi.ProwJobSpec{JobQueueName: "queue"}},
  2148  			JobQueueCapacities: map[string]int{"queue": -1},
  2149  			ExpectedResult:     true,
  2150  		},
  2151  		{
  2152  			Name: "Num pending within max concurrency but exceeds job queue concurrency",
  2153  			ProwJob: prowapi.ProwJob{
  2154  				ObjectMeta: metav1.ObjectMeta{CreationTimestamp: metav1.Now()},
  2155  				Spec: prowapi.ProwJobSpec{
  2156  					MaxConcurrency: 100,
  2157  					Job:            "my-pj",
  2158  					JobQueueName:   "queue",
  2159  				},
  2160  			},
  2161  			JobQueueCapacities: map[string]int{"queue": 10},
  2162  			PendingJobs:        map[string]pendingJob{"my-pj": {Duplicates: 10, JobQueue: "queue"}},
  2163  			ExpectedResult:     false,
  2164  		},
  2165  	}
  2166  
  2167  	for _, tc := range testCases {
  2168  		t.Run(tc.Name, func(t *testing.T) {
  2169  			if tc.PendingJobs == nil {
  2170  				tc.PendingJobs = map[string]pendingJob{}
  2171  			}
  2172  			buildClients := map[string]buildClient{}
  2173  			logrus.SetLevel(logrus.DebugLevel)
  2174  
  2175  			builder := fakectrlruntimeclient.NewClientBuilder()
  2176  
  2177  			for i := range tc.ExistingProwJobs {
  2178  				tc.ExistingProwJobs[i].Namespace = "prowjobs"
  2179  				builder.WithRuntimeObjects(&tc.ExistingProwJobs[i])
  2180  			}
  2181  
  2182  			for jobName, jobsToCreateParams := range tc.PendingJobs {
  2183  				for i := 0; i < jobsToCreateParams.Duplicates; i++ {
  2184  					builder.WithRuntimeObjects(&prowapi.ProwJob{
  2185  						ObjectMeta: metav1.ObjectMeta{
  2186  							Name:      fmt.Sprintf("%s-%d", jobName, i),
  2187  							Namespace: "prowjobs",
  2188  						},
  2189  						Spec: prowapi.ProwJobSpec{
  2190  							Agent:        prowapi.KubernetesAgent,
  2191  							Job:          jobName,
  2192  							JobQueueName: jobsToCreateParams.JobQueue,
  2193  						},
  2194  						Status: prowapi.ProwJobStatus{
  2195  							State: prowapi.PendingState,
  2196  						},
  2197  					})
  2198  				}
  2199  			}
  2200  			r := &reconciler{
  2201  				pjClient: &indexingClient{
  2202  					Client:     builder.Build(),
  2203  					indexFuncs: map[string]ctrlruntimeclient.IndexerFunc{prowJobIndexName: prowJobIndexer("prowjobs")},
  2204  				},
  2205  				buildClients: buildClients,
  2206  				log:          logrus.NewEntry(logrus.StandardLogger()),
  2207  				config:       newFakeConfigAgent(t, 0, tc.JobQueueCapacities).Config,
  2208  				clock:        clock.RealClock{},
  2209  			}
  2210  			// We filter ourselves out via the UID, so make sure its not the empty string
  2211  			tc.ProwJob.UID = types.UID("under-test")
  2212  			result, err := r.canExecuteConcurrently(context.Background(), &tc.ProwJob)
  2213  			if err != nil {
  2214  				t.Fatalf("canExecuteConcurrently: %v", err)
  2215  			}
  2216  
  2217  			if result != tc.ExpectedResult {
  2218  				t.Errorf("Expected max_concurrency to allow job: %t, result was %t", tc.ExpectedResult, result)
  2219  			}
  2220  		})
  2221  	}
  2222  }
  2223  
  2224  type patchTrackingFakeClient struct {
  2225  	ctrlruntimeclient.Client
  2226  	patched sets.Set[string]
  2227  }
  2228  
  2229  func (c *patchTrackingFakeClient) Patch(ctx context.Context, obj ctrlruntimeclient.Object, patch ctrlruntimeclient.Patch, opts ...ctrlruntimeclient.PatchOption) error {
  2230  	if c.patched == nil {
  2231  		c.patched = sets.New[string]()
  2232  	}
  2233  	c.patched.Insert(obj.GetName())
  2234  	return c.Client.Patch(ctx, obj, patch, opts...)
  2235  }
  2236  
  2237  type deleteTrackingFakeClient struct {
  2238  	deleteError error
  2239  	ctrlruntimeclient.Client
  2240  	deleted sets.Set[string]
  2241  }
  2242  
  2243  func (c *deleteTrackingFakeClient) Delete(ctx context.Context, obj ctrlruntimeclient.Object, opts ...ctrlruntimeclient.DeleteOption) error {
  2244  	if c.deleteError != nil {
  2245  		return c.deleteError
  2246  	}
  2247  	if c.deleted == nil {
  2248  		c.deleted = sets.Set[string]{}
  2249  	}
  2250  	if err := c.Client.Delete(ctx, obj, opts...); err != nil {
  2251  		return err
  2252  	}
  2253  	c.deleted.Insert(obj.GetName())
  2254  	return nil
  2255  }
  2256  
  2257  type clientWrapper struct {
  2258  	ctrlruntimeclient.Client
  2259  	createError              error
  2260  	errOnDeleteWithFinalizer bool
  2261  }
  2262  
  2263  func (c *clientWrapper) Create(ctx context.Context, obj ctrlruntimeclient.Object, opts ...ctrlruntimeclient.CreateOption) error {
  2264  	if c.createError != nil {
  2265  		return c.createError
  2266  	}
  2267  	return c.Client.Create(ctx, obj, opts...)
  2268  }
  2269  
  2270  func (c *clientWrapper) Delete(ctx context.Context, obj ctrlruntimeclient.Object, opts ...ctrlruntimeclient.DeleteOption) error {
  2271  	if len(obj.GetFinalizers()) > 0 {
  2272  		return fmt.Errorf("object still had finalizers when attempting to delete: %v", obj.GetFinalizers())
  2273  	}
  2274  	return c.Client.Delete(ctx, obj, opts...)
  2275  }
  2276  
  2277  func TestSyncAbortedJob(t *testing.T) {
  2278  	t.Parallel()
  2279  
  2280  	type testCase struct {
  2281  		Name           string
  2282  		Pod            *v1.Pod
  2283  		DeleteError    error
  2284  		ExpectSyncFail bool
  2285  		ExpectDelete   bool
  2286  		ExpectComplete bool
  2287  	}
  2288  
  2289  	testCases := []testCase{
  2290  		{
  2291  			Name:           "Pod is deleted",
  2292  			Pod:            &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "my-pj"}},
  2293  			ExpectDelete:   true,
  2294  			ExpectComplete: true,
  2295  		},
  2296  		{
  2297  			Name:           "No pod there",
  2298  			ExpectDelete:   false,
  2299  			ExpectComplete: true,
  2300  		},
  2301  		{
  2302  			Name:           "NotFound on delete is tolerated",
  2303  			Pod:            &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "my-pj"}},
  2304  			DeleteError:    kapierrors.NewNotFound(schema.GroupResource{}, "my-pj"),
  2305  			ExpectDelete:   false,
  2306  			ExpectComplete: true,
  2307  		},
  2308  		{
  2309  			Name:           "Failed delete does not set job to completed",
  2310  			Pod:            &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "my-pj"}},
  2311  			DeleteError:    errors.New("erroring as requested"),
  2312  			ExpectSyncFail: true,
  2313  			ExpectDelete:   false,
  2314  			ExpectComplete: false,
  2315  		},
  2316  	}
  2317  
  2318  	const cluster = "cluster"
  2319  	for _, tc := range testCases {
  2320  		t.Run(tc.Name, func(t *testing.T) {
  2321  			pj := &prowapi.ProwJob{
  2322  				ObjectMeta: metav1.ObjectMeta{
  2323  					Name: "my-pj",
  2324  				},
  2325  				Spec: prowapi.ProwJobSpec{
  2326  					Cluster: cluster,
  2327  				},
  2328  				Status: prowapi.ProwJobStatus{
  2329  					State: prowapi.AbortedState,
  2330  				},
  2331  			}
  2332  
  2333  			builder := fakectrlruntimeclient.NewClientBuilder()
  2334  			if tc.Pod != nil {
  2335  				builder.WithRuntimeObjects(tc.Pod)
  2336  			}
  2337  			podClient := &deleteTrackingFakeClient{
  2338  				deleteError: tc.DeleteError,
  2339  				Client:      builder.Build(),
  2340  			}
  2341  
  2342  			pjClient := fakectrlruntimeclient.NewClientBuilder().WithRuntimeObjects(pj).Build()
  2343  			r := &reconciler{
  2344  				log:          logrus.NewEntry(logrus.New()),
  2345  				config:       func() *config.Config { return &config.Config{} },
  2346  				pjClient:     pjClient,
  2347  				buildClients: map[string]buildClient{cluster: {Client: podClient}},
  2348  			}
  2349  
  2350  			res, err := r.reconcile(context.Background(), pj)
  2351  			if (err != nil) != tc.ExpectSyncFail {
  2352  				t.Fatalf("sync failed: %v, expected it to fail: %t", err, tc.ExpectSyncFail)
  2353  			}
  2354  			if res != nil {
  2355  				t.Errorf("expected reconcile.Result to be nil, was %v", res)
  2356  			}
  2357  
  2358  			if err := pjClient.Get(context.Background(), types.NamespacedName{Name: pj.Name}, pj); err != nil {
  2359  				t.Fatalf("failed to get job from client: %v", err)
  2360  			}
  2361  			if pj.Complete() != tc.ExpectComplete {
  2362  				t.Errorf("expected complete: %t, got complete: %t", tc.ExpectComplete, pj.Complete())
  2363  			}
  2364  
  2365  			if tc.ExpectDelete != podClient.deleted.Has(pj.Name) {
  2366  				t.Errorf("expected delete: %t, got delete: %t", tc.ExpectDelete, podClient.deleted.Has(pj.Name))
  2367  			}
  2368  		})
  2369  	}
  2370  }
  2371  
  2372  type indexingClient struct {
  2373  	ctrlruntimeclient.Client
  2374  	indexFuncs map[string]ctrlruntimeclient.IndexerFunc
  2375  }
  2376  
  2377  func (c *indexingClient) List(ctx context.Context, list ctrlruntimeclient.ObjectList, opts ...ctrlruntimeclient.ListOption) error {
  2378  	if err := c.Client.List(ctx, list, opts...); err != nil {
  2379  		return err
  2380  	}
  2381  
  2382  	listOpts := &ctrlruntimeclient.ListOptions{}
  2383  	for _, opt := range opts {
  2384  		opt.ApplyToList(listOpts)
  2385  	}
  2386  
  2387  	if listOpts.FieldSelector == nil {
  2388  		return nil
  2389  	}
  2390  
  2391  	if n := len(listOpts.FieldSelector.Requirements()); n == 0 {
  2392  		return nil
  2393  	} else if n > 1 {
  2394  		return fmt.Errorf("the indexing client supports at most one field selector requirement, got %d", n)
  2395  	}
  2396  
  2397  	indexKey := listOpts.FieldSelector.Requirements()[0].Field
  2398  	if indexKey == "" {
  2399  		return nil
  2400  	}
  2401  
  2402  	indexFunc, ok := c.indexFuncs[indexKey]
  2403  	if !ok {
  2404  		return fmt.Errorf("no index with key %q found", indexKey)
  2405  	}
  2406  
  2407  	pjList, ok := list.(*prowapi.ProwJobList)
  2408  	if !ok {
  2409  		return errors.New("indexes are only supported for ProwJobLists")
  2410  	}
  2411  
  2412  	result := prowapi.ProwJobList{}
  2413  	for _, pj := range pjList.Items {
  2414  		for _, indexVal := range indexFunc(&pj) {
  2415  			logrus.Infof("indexVal: %q, requirementVal: %q, match: %t, name: %s", indexVal, listOpts.FieldSelector.Requirements()[0].Value, indexVal == listOpts.FieldSelector.Requirements()[0].Value, pj.Name)
  2416  			if indexVal == listOpts.FieldSelector.Requirements()[0].Value {
  2417  				result.Items = append(result.Items, pj)
  2418  			}
  2419  		}
  2420  	}
  2421  
  2422  	*pjList = result
  2423  	return nil
  2424  }
  2425  
  2426  func TestPredicates(t *testing.T) {
  2427  	for _, tc := range []struct {
  2428  		name       string
  2429  		obj        ctrlruntimeclient.Object
  2430  		selector   string
  2431  		wantResult bool
  2432  	}{
  2433  		{
  2434  			name:       "Accept PJ",
  2435  			obj:        &prowapi.ProwJob{Spec: prowapi.ProwJobSpec{Agent: prowapi.KubernetesAgent}},
  2436  			wantResult: true,
  2437  		},
  2438  		{
  2439  			name:       "Accept Pod if created by Prow",
  2440  			obj:        &v1.Pod{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{kube.CreatedByProw: "true"}}},
  2441  			wantResult: true,
  2442  		},
  2443  		{
  2444  			name:       "Accept Pod if matches additional selector",
  2445  			obj:        &v1.Pod{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{kube.CreatedByProw: "true", "foo": "bar"}}},
  2446  			selector:   "foo=bar",
  2447  			wantResult: true,
  2448  		},
  2449  		{
  2450  			name: "Filter scheduling",
  2451  			obj: &prowapi.ProwJob{
  2452  				Spec:   prowapi.ProwJobSpec{Agent: prowapi.KubernetesAgent},
  2453  				Status: prowapi.ProwJobStatus{State: prowapi.SchedulingState},
  2454  			},
  2455  		},
  2456  		{
  2457  			name: "Filter completed",
  2458  			obj: &prowapi.ProwJob{
  2459  				Spec:   prowapi.ProwJobSpec{Agent: prowapi.KubernetesAgent},
  2460  				Status: prowapi.ProwJobStatus{CompletionTime: &metav1.Time{}},
  2461  			},
  2462  		},
  2463  		{
  2464  			name: "Filter non k8s agent",
  2465  			obj:  &prowapi.ProwJob{Spec: prowapi.ProwJobSpec{Agent: prowapi.JenkinsAgent}},
  2466  		},
  2467  	} {
  2468  		t.Run(tc.name, func(t *testing.T) {
  2469  			predicate, err := predicates(tc.selector, nil)
  2470  			if err != nil {
  2471  				t.Fatalf("Unexpected error %s", err)
  2472  			}
  2473  
  2474  			actualResult := predicate.Create(event.CreateEvent{Object: tc.obj}) &&
  2475  				predicate.Update(event.UpdateEvent{ObjectNew: tc.obj}) &&
  2476  				predicate.Delete(event.DeleteEvent{Object: tc.obj}) &&
  2477  				predicate.Generic(event.GenericEvent{Object: tc.obj})
  2478  
  2479  			if actualResult != tc.wantResult {
  2480  				t.Errorf("Expected %t but got %t", tc.wantResult, actualResult)
  2481  			}
  2482  		})
  2483  	}
  2484  }