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 }