sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/jenkins/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 jenkins 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "net/http" 24 "net/http/httptest" 25 "reflect" 26 "sync" 27 "testing" 28 "text/template" 29 "time" 30 31 "github.com/sirupsen/logrus" 32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apimachinery/pkg/runtime" 34 "k8s.io/utils/clock" 35 clocktesting "k8s.io/utils/clock/testing" 36 "sigs.k8s.io/prow/pkg/client/clientset/versioned/fake" 37 38 prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 39 "sigs.k8s.io/prow/pkg/config" 40 "sigs.k8s.io/prow/pkg/github" 41 "sigs.k8s.io/prow/pkg/pjutil" 42 ) 43 44 type fca struct { 45 sync.Mutex 46 c *config.Config 47 } 48 49 func newFakeConfigAgent(t *testing.T, maxConcurrency int, operators []config.JenkinsOperator) *fca { 50 presubmits := []config.Presubmit{ 51 { 52 JobBase: config.JobBase{ 53 Name: "test-bazel-build", 54 }, 55 }, 56 { 57 JobBase: config.JobBase{ 58 Name: "test-e2e", 59 }, 60 }, 61 { 62 AlwaysRun: true, 63 JobBase: config.JobBase{ 64 Name: "test-bazel-test", 65 }, 66 }, 67 } 68 if err := config.SetPresubmitRegexes(presubmits); err != nil { 69 t.Fatal(err) 70 } 71 presubmitMap := map[string][]config.Presubmit{ 72 "kubernetes/kubernetes": presubmits, 73 } 74 75 ca := &fca{ 76 c: &config.Config{ 77 ProwConfig: config.ProwConfig{ 78 ProwJobNamespace: "prowjobs", 79 JenkinsOperators: []config.JenkinsOperator{ 80 { 81 Controller: config.Controller{ 82 JobURLTemplate: template.Must(template.New("test").Parse("{{.Status.PodName}}/{{.Status.State}}")), 83 MaxConcurrency: maxConcurrency, 84 MaxGoroutines: 20, 85 }, 86 }, 87 }, 88 StatusErrorLink: "https://github.com/kubernetes/test-infra/issues", 89 }, 90 JobConfig: config.JobConfig{ 91 PresubmitsStatic: presubmitMap, 92 }, 93 }, 94 } 95 if len(operators) > 0 { 96 ca.c.JenkinsOperators = operators 97 } 98 return ca 99 } 100 101 func (f *fca) Config() *config.Config { 102 f.Lock() 103 defer f.Unlock() 104 return f.c 105 } 106 107 type fjc struct { 108 sync.Mutex 109 built bool 110 pjs []prowapi.ProwJob 111 err error 112 builds map[string]Build 113 didAbort bool 114 abortErrors bool 115 } 116 117 func (f *fjc) Build(pj *prowapi.ProwJob, buildID string) error { 118 f.Lock() 119 defer f.Unlock() 120 if f.err != nil { 121 return f.err 122 } 123 f.built = true 124 f.pjs = append(f.pjs, *pj) 125 return nil 126 } 127 128 func (f *fjc) ListBuilds(jobs []BuildQueryParams) (map[string]Build, error) { 129 f.Lock() 130 defer f.Unlock() 131 if f.err != nil { 132 return nil, f.err 133 } 134 return f.builds, nil 135 } 136 137 func (f *fjc) Abort(job string, build *Build) error { 138 f.Lock() 139 defer f.Unlock() 140 if f.abortErrors { 141 return errors.New("erroring on abort as requested") 142 } 143 f.didAbort = true 144 return nil 145 } 146 147 type fghc struct { 148 sync.Mutex 149 changes []github.PullRequestChange 150 err error 151 } 152 153 func (f *fghc) GetPullRequestChanges(org, repo string, number int) ([]github.PullRequestChange, error) { 154 f.Lock() 155 defer f.Unlock() 156 return f.changes, f.err 157 } 158 159 func (f *fghc) BotUserCheckerWithContext(context.Context) (func(string) bool, error) { 160 return func(candidate string) bool { 161 return candidate == "bot" 162 }, nil 163 } 164 func (f *fghc) CreateStatusWithContext(_ context.Context, org, repo, ref string, s github.Status) error { 165 f.Lock() 166 defer f.Unlock() 167 return nil 168 } 169 func (f *fghc) ListIssueCommentsWithContext(_ context.Context, org, repo string, number int) ([]github.IssueComment, error) { 170 f.Lock() 171 defer f.Unlock() 172 return nil, nil 173 } 174 func (f *fghc) CreateCommentWithContext(_ context.Context, org, repo string, number int, comment string) error { 175 f.Lock() 176 defer f.Unlock() 177 return nil 178 } 179 func (f *fghc) DeleteCommentWithContext(_ context.Context, org, repo string, ID int) error { 180 f.Lock() 181 defer f.Unlock() 182 return nil 183 } 184 func (f *fghc) EditCommentWithContext(_ context.Context, org, repo string, ID int, comment string) error { 185 f.Lock() 186 defer f.Unlock() 187 return nil 188 } 189 190 func TestSyncTriggeredJobs(t *testing.T) { 191 fakeClock := clocktesting.NewFakeClock(time.Now().Truncate(1 * time.Second)) 192 pendingTime := metav1.NewTime(fakeClock.Now()) 193 194 var testcases = []struct { 195 name string 196 pj prowapi.ProwJob 197 pendingJobs map[string]int 198 maxConcurrency int 199 builds map[string]Build 200 err error 201 202 expectedState prowapi.ProwJobState 203 expectedBuild bool 204 expectedComplete bool 205 expectedReport bool 206 expectedEnqueued bool 207 expectedError bool 208 expectedPendingTime *metav1.Time 209 }{ 210 { 211 name: "start new job", 212 pj: prowapi.ProwJob{ 213 ObjectMeta: metav1.ObjectMeta{ 214 Name: "test", 215 Namespace: "prowjobs", 216 }, 217 Spec: prowapi.ProwJobSpec{ 218 Type: prowapi.PostsubmitJob, 219 }, 220 Status: prowapi.ProwJobStatus{ 221 State: prowapi.TriggeredState, 222 }, 223 }, 224 expectedBuild: true, 225 expectedReport: true, 226 expectedState: prowapi.PendingState, 227 expectedEnqueued: true, 228 expectedPendingTime: &pendingTime, 229 }, 230 { 231 name: "start new job, error", 232 pj: prowapi.ProwJob{ 233 ObjectMeta: metav1.ObjectMeta{ 234 Name: "test", 235 Namespace: "prowjobs", 236 }, 237 Spec: prowapi.ProwJobSpec{ 238 Type: prowapi.PresubmitJob, 239 Refs: &prowapi.Refs{ 240 Pulls: []prowapi.Pull{{ 241 Number: 1, 242 SHA: "fake-sha", 243 }}, 244 }, 245 }, 246 Status: prowapi.ProwJobStatus{ 247 State: prowapi.TriggeredState, 248 }, 249 }, 250 err: errors.New("oh no"), 251 expectedReport: true, 252 expectedState: prowapi.ErrorState, 253 expectedComplete: true, 254 expectedError: true, 255 }, 256 { 257 name: "block running new job", 258 pj: prowapi.ProwJob{ 259 ObjectMeta: metav1.ObjectMeta{ 260 Name: "test", 261 Namespace: "prowjobs", 262 }, 263 Spec: prowapi.ProwJobSpec{ 264 Type: prowapi.PostsubmitJob, 265 }, 266 Status: prowapi.ProwJobStatus{ 267 State: prowapi.TriggeredState, 268 }, 269 }, 270 pendingJobs: map[string]int{"motherearth": 10, "allagash": 8, "krusovice": 2}, 271 maxConcurrency: 20, 272 expectedBuild: false, 273 expectedReport: false, 274 expectedState: prowapi.TriggeredState, 275 expectedEnqueued: false, 276 }, 277 { 278 name: "allow running new job", 279 pj: prowapi.ProwJob{ 280 ObjectMeta: metav1.ObjectMeta{ 281 Name: "test", 282 Namespace: "prowjobs", 283 }, 284 Spec: prowapi.ProwJobSpec{ 285 Type: prowapi.PostsubmitJob, 286 }, 287 Status: prowapi.ProwJobStatus{ 288 State: prowapi.TriggeredState, 289 }, 290 }, 291 pendingJobs: map[string]int{"motherearth": 10, "allagash": 8, "krusovice": 2}, 292 maxConcurrency: 21, 293 expectedBuild: true, 294 expectedReport: true, 295 expectedState: prowapi.PendingState, 296 expectedEnqueued: true, 297 expectedPendingTime: &pendingTime, 298 }, 299 } 300 for _, tc := range testcases { 301 totServ := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 302 fmt.Fprint(w, "42") 303 })) 304 defer totServ.Close() 305 t.Logf("scenario %q", tc.name) 306 fjc := &fjc{ 307 err: tc.err, 308 } 309 fakeProwJobClient := fake.NewSimpleClientset(&tc.pj) 310 311 c := Controller{ 312 prowJobClient: fakeProwJobClient.ProwV1().ProwJobs("prowjobs"), 313 jc: fjc, 314 log: logrus.NewEntry(logrus.StandardLogger()), 315 cfg: newFakeConfigAgent(t, tc.maxConcurrency, nil).Config, 316 totURL: totServ.URL, 317 lock: sync.RWMutex{}, 318 pendingJobs: make(map[string]int), 319 clock: fakeClock, 320 } 321 if tc.pendingJobs != nil { 322 c.pendingJobs = tc.pendingJobs 323 } 324 325 reports := make(chan prowapi.ProwJob, 100) 326 if err := c.syncTriggeredJob(tc.pj, reports, tc.builds); err != nil { 327 t.Errorf("unexpected error: %v", err) 328 continue 329 } 330 close(reports) 331 332 actualProwJobs, err := fakeProwJobClient.ProwV1().ProwJobs("prowjobs").List(context.Background(), metav1.ListOptions{}) 333 if err != nil { 334 t.Fatalf("failed to list prowjobs from client %v", err) 335 } 336 if len(actualProwJobs.Items) != 1 { 337 t.Fatalf("Didn't create just one ProwJob, but %d", len(actualProwJobs.Items)) 338 } 339 actual := actualProwJobs.Items[0] 340 if tc.expectedError && actual.Status.Description != "Error starting Jenkins job." { 341 t.Errorf("expected description %q, got %q", "Error starting Jenkins job.", actual.Status.Description) 342 continue 343 } 344 if actual.Status.State != tc.expectedState { 345 t.Errorf("expected state %q, got %q", tc.expectedState, actual.Status.State) 346 continue 347 } 348 if actual.Complete() != tc.expectedComplete { 349 t.Errorf("expected complete prowjob, got %v", actual) 350 continue 351 } 352 if tc.expectedReport && len(reports) != 1 { 353 t.Errorf("wanted one report but got %d", len(reports)) 354 continue 355 } 356 if !tc.expectedReport && len(reports) != 0 { 357 t.Errorf("did not wany any reports but got %d", len(reports)) 358 continue 359 } 360 if fjc.built != tc.expectedBuild { 361 t.Errorf("expected build: %t, got: %t", tc.expectedBuild, fjc.built) 362 continue 363 } 364 if tc.expectedEnqueued && actual.Status.Description != "Jenkins job enqueued." { 365 t.Errorf("expected enqueued prowjob, got %v", actual) 366 } 367 if !reflect.DeepEqual(actual.Status.PendingTime, tc.expectedPendingTime) { 368 t.Errorf("for case %q got pending time %v, expected %v", tc.name, actual.Status.PendingTime, tc.expectedPendingTime) 369 } 370 } 371 } 372 373 func TestSyncPendingJobs(t *testing.T) { 374 var testcases = []struct { 375 name string 376 pj prowapi.ProwJob 377 pendingJobs map[string]int 378 builds map[string]Build 379 err error 380 381 // TODO: Change to pass a ProwJobStatus 382 expectedState prowapi.ProwJobState 383 expectedBuild bool 384 expectedURL string 385 expectedComplete bool 386 expectedReport bool 387 expectedEnqueued bool 388 expectedError bool 389 }{ 390 { 391 name: "enqueued", 392 pj: prowapi.ProwJob{ 393 ObjectMeta: metav1.ObjectMeta{ 394 Name: "foofoo", 395 Namespace: "prowjobs", 396 }, 397 Spec: prowapi.ProwJobSpec{ 398 Job: "test-job", 399 }, 400 Status: prowapi.ProwJobStatus{ 401 State: prowapi.PendingState, 402 Description: "Jenkins job enqueued.", 403 }, 404 }, 405 builds: map[string]Build{ 406 "foofoo": {enqueued: true, Number: 10}, 407 }, 408 expectedState: prowapi.PendingState, 409 expectedEnqueued: true, 410 }, 411 { 412 name: "finished queue", 413 pj: prowapi.ProwJob{ 414 ObjectMeta: metav1.ObjectMeta{ 415 Name: "boing", 416 Namespace: "prowjobs", 417 }, 418 Spec: prowapi.ProwJobSpec{ 419 Job: "test-job", 420 }, 421 Status: prowapi.ProwJobStatus{ 422 State: prowapi.PendingState, 423 Description: "Jenkins job enqueued.", 424 }, 425 }, 426 builds: map[string]Build{ 427 "boing": {enqueued: false, Number: 10}, 428 }, 429 expectedURL: "boing/pending", 430 expectedState: prowapi.PendingState, 431 expectedEnqueued: false, 432 expectedReport: true, 433 }, 434 { 435 name: "building", 436 pj: prowapi.ProwJob{ 437 ObjectMeta: metav1.ObjectMeta{ 438 Name: "firstoutthetrenches", 439 Namespace: "prowjobs", 440 }, 441 Spec: prowapi.ProwJobSpec{ 442 Job: "test-job", 443 }, 444 Status: prowapi.ProwJobStatus{ 445 State: prowapi.PendingState, 446 }, 447 }, 448 builds: map[string]Build{ 449 "firstoutthetrenches": {enqueued: false, Number: 10}, 450 }, 451 expectedURL: "firstoutthetrenches/pending", 452 expectedState: prowapi.PendingState, 453 expectedReport: true, 454 }, 455 { 456 name: "missing build", 457 pj: prowapi.ProwJob{ 458 ObjectMeta: metav1.ObjectMeta{ 459 Name: "blabla", 460 Namespace: "prowjobs", 461 }, 462 Spec: prowapi.ProwJobSpec{ 463 Type: prowapi.PresubmitJob, 464 Job: "test-job", 465 Refs: &prowapi.Refs{ 466 Pulls: []prowapi.Pull{{ 467 Number: 1, 468 SHA: "fake-sha", 469 }}, 470 }, 471 }, 472 Status: prowapi.ProwJobStatus{ 473 State: prowapi.PendingState, 474 }, 475 }, 476 // missing build 477 builds: map[string]Build{ 478 "other": {enqueued: false, Number: 10}, 479 }, 480 expectedURL: "https://github.com/kubernetes/test-infra/issues", 481 expectedState: prowapi.ErrorState, 482 expectedError: true, 483 expectedComplete: true, 484 expectedReport: true, 485 }, 486 { 487 name: "finished, success", 488 pj: prowapi.ProwJob{ 489 ObjectMeta: metav1.ObjectMeta{ 490 Name: "winwin", 491 Namespace: "prowjobs", 492 }, 493 Spec: prowapi.ProwJobSpec{ 494 Job: "test-job", 495 }, 496 Status: prowapi.ProwJobStatus{ 497 State: prowapi.PendingState, 498 }, 499 }, 500 builds: map[string]Build{ 501 "winwin": {Result: pState(success), Number: 11}, 502 }, 503 expectedURL: "winwin/success", 504 expectedState: prowapi.SuccessState, 505 expectedComplete: true, 506 expectedReport: true, 507 }, 508 { 509 name: "finished, failed", 510 pj: prowapi.ProwJob{ 511 ObjectMeta: metav1.ObjectMeta{ 512 Name: "whatapity", 513 Namespace: "prowjobs", 514 }, 515 Spec: prowapi.ProwJobSpec{ 516 Job: "test-job", 517 }, 518 Status: prowapi.ProwJobStatus{ 519 State: prowapi.PendingState, 520 }, 521 }, 522 builds: map[string]Build{ 523 "whatapity": {Result: pState(failure), Number: 12}, 524 }, 525 expectedURL: "whatapity/failure", 526 expectedState: prowapi.FailureState, 527 expectedComplete: true, 528 expectedReport: true, 529 }, 530 } 531 for _, tc := range testcases { 532 t.Logf("scenario %q", tc.name) 533 totServ := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 534 fmt.Fprint(w, "42") 535 })) 536 defer totServ.Close() 537 fjc := &fjc{ 538 err: tc.err, 539 } 540 fakeProwJobClient := fake.NewSimpleClientset(&tc.pj) 541 542 c := Controller{ 543 prowJobClient: fakeProwJobClient.ProwV1().ProwJobs("prowjobs"), 544 jc: fjc, 545 log: logrus.NewEntry(logrus.StandardLogger()), 546 cfg: newFakeConfigAgent(t, 0, nil).Config, 547 totURL: totServ.URL, 548 lock: sync.RWMutex{}, 549 pendingJobs: make(map[string]int), 550 clock: clock.RealClock{}, 551 } 552 553 reports := make(chan prowapi.ProwJob, 100) 554 if err := c.syncPendingJob(tc.pj, reports, tc.builds); err != nil { 555 t.Errorf("unexpected error: %v", err) 556 continue 557 } 558 close(reports) 559 560 actualProwJobs, err := fakeProwJobClient.ProwV1().ProwJobs("prowjobs").List(context.Background(), metav1.ListOptions{}) 561 if err != nil { 562 t.Fatalf("failed to list prowjobs from client %v", err) 563 } 564 if len(actualProwJobs.Items) != 1 { 565 t.Fatalf("Didn't create just one ProwJob, but %d", len(actualProwJobs.Items)) 566 } 567 actual := actualProwJobs.Items[0] 568 if tc.expectedError && actual.Status.Description != "Error finding Jenkins job." { 569 t.Errorf("expected description %q, got %q", "Error finding Jenkins job.", actual.Status.Description) 570 continue 571 } 572 if actual.Status.State != tc.expectedState { 573 t.Errorf("expected state %q, got %q", tc.expectedState, actual.Status.State) 574 continue 575 } 576 if actual.Complete() != tc.expectedComplete { 577 t.Errorf("expected complete prowjob, got %v", actual) 578 continue 579 } 580 if tc.expectedReport && len(reports) != 1 { 581 t.Errorf("wanted one report but got %d", len(reports)) 582 continue 583 } 584 if !tc.expectedReport && len(reports) != 0 { 585 t.Errorf("did not wany any reports but got %d", len(reports)) 586 continue 587 } 588 if fjc.built != tc.expectedBuild { 589 t.Errorf("expected build: %t, got: %t", tc.expectedBuild, fjc.built) 590 continue 591 } 592 if tc.expectedEnqueued && actual.Status.Description != "Jenkins job enqueued." { 593 t.Errorf("expected enqueued prowjob, got %v", actual) 594 } 595 if tc.expectedURL != actual.Status.URL { 596 t.Errorf("expected status URL: %s, got: %s", tc.expectedURL, actual.Status.URL) 597 } 598 } 599 } 600 601 func pState(state string) *string { 602 s := state 603 return &s 604 } 605 606 // TestBatch walks through the happy path of a batch job on Jenkins. 607 func TestBatch(t *testing.T) { 608 pre := config.Presubmit{ 609 JobBase: config.JobBase{ 610 Name: "pr-some-job", 611 Agent: "jenkins", 612 }, 613 Reporter: config.Reporter{ 614 Context: "Some Job Context", 615 }, 616 } 617 pj := pjutil.NewProwJob(pjutil.BatchSpec(pre, prowapi.Refs{ 618 Org: "o", 619 Repo: "r", 620 BaseRef: "master", 621 BaseSHA: "123", 622 Pulls: []prowapi.Pull{ 623 { 624 Number: 1, 625 SHA: "abc", 626 }, 627 { 628 Number: 2, 629 SHA: "qwe", 630 }, 631 }, 632 }), nil, nil) 633 pj.ObjectMeta.Name = "known_name" 634 pj.ObjectMeta.Namespace = "prowjobs" 635 fakeProwJobClient := fake.NewSimpleClientset(&pj) 636 jc := &fjc{ 637 builds: map[string]Build{ 638 "known_name": { /* Running */ }, 639 }, 640 } 641 totServ := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 642 fmt.Fprint(w, "42") 643 })) 644 defer totServ.Close() 645 c := Controller{ 646 prowJobClient: fakeProwJobClient.ProwV1().ProwJobs("prowjobs"), 647 ghc: &fghc{}, 648 jc: jc, 649 log: logrus.NewEntry(logrus.StandardLogger()), 650 cfg: newFakeConfigAgent(t, 0, nil).Config, 651 totURL: totServ.URL, 652 pendingJobs: make(map[string]int), 653 lock: sync.RWMutex{}, 654 clock: clock.RealClock{}, 655 } 656 657 if err := c.Sync(); err != nil { 658 t.Fatalf("Error on first sync: %v", err) 659 } 660 afterFirstSync, err := fakeProwJobClient.ProwV1().ProwJobs("prowjobs").Get(context.Background(), "known_name", metav1.GetOptions{}) 661 if err != nil { 662 t.Fatalf("failed to get prowjob from client: %v", err) 663 } 664 if afterFirstSync.Status.State != prowapi.PendingState { 665 t.Fatalf("Wrong state: %v", afterFirstSync.Status.State) 666 } 667 if afterFirstSync.Status.Description != "Jenkins job enqueued." { 668 t.Fatalf("Expected description %q, got %q.", "Jenkins job enqueued.", afterFirstSync.Status.Description) 669 } 670 jc.builds["known_name"] = Build{Number: 42} 671 if err := c.Sync(); err != nil { 672 t.Fatalf("Error on second sync: %v", err) 673 } 674 afterSecondSync, err := fakeProwJobClient.ProwV1().ProwJobs("prowjobs").Get(context.Background(), "known_name", metav1.GetOptions{}) 675 if err != nil { 676 t.Fatalf("failed to get prowjob from client: %v", err) 677 } 678 if afterSecondSync.Status.Description != "Jenkins job running." { 679 t.Fatalf("Expected description %q, got %q.", "Jenkins job running.", afterSecondSync.Status.Description) 680 } 681 if afterSecondSync.Status.PodName != "known_name" { 682 t.Fatalf("Wrong PodName: %s", afterSecondSync.Status.PodName) 683 } 684 jc.builds["known_name"] = Build{Result: pState(success)} 685 if err := c.Sync(); err != nil { 686 t.Fatalf("Error on third sync: %v", err) 687 } 688 afterThirdSync, err := fakeProwJobClient.ProwV1().ProwJobs("prowjobs").Get(context.Background(), "known_name", metav1.GetOptions{}) 689 if err != nil { 690 t.Fatalf("failed to get prowjob from client: %v", err) 691 } 692 if afterThirdSync.Status.Description != "Jenkins job succeeded." { 693 t.Fatalf("Expected description %q, got %q.", "Jenkins job succeeded.", afterThirdSync.Status.Description) 694 } 695 if afterThirdSync.Status.State != prowapi.SuccessState { 696 t.Fatalf("Wrong state: %v", afterThirdSync.Status.State) 697 } 698 // This is what the SQ reads. 699 if afterThirdSync.Spec.Context != "Some Job Context" { 700 t.Fatalf("Wrong context: %v", afterThirdSync.Spec.Context) 701 } 702 } 703 704 func TestMaxConcurrencyWithNewlyTriggeredJobs(t *testing.T) { 705 tests := []struct { 706 name string 707 pjs []prowapi.ProwJob 708 pendingJobs map[string]int 709 expectedBuilds int 710 }{ 711 { 712 name: "avoid starting a triggered job", 713 pjs: []prowapi.ProwJob{ 714 { 715 ObjectMeta: metav1.ObjectMeta{ 716 Name: "first", 717 Namespace: "prowjobs", 718 }, 719 Spec: prowapi.ProwJobSpec{ 720 Job: "test-bazel-build", 721 Type: prowapi.PostsubmitJob, 722 MaxConcurrency: 1, 723 }, 724 Status: prowapi.ProwJobStatus{ 725 State: prowapi.TriggeredState, 726 }, 727 }, 728 { 729 ObjectMeta: metav1.ObjectMeta{ 730 Name: "second", 731 Namespace: "prowjobs", 732 }, 733 Spec: prowapi.ProwJobSpec{ 734 Job: "test-bazel-build", 735 Type: prowapi.PostsubmitJob, 736 MaxConcurrency: 1, 737 }, 738 Status: prowapi.ProwJobStatus{ 739 State: prowapi.TriggeredState, 740 }, 741 }, 742 }, 743 pendingJobs: make(map[string]int), 744 expectedBuilds: 1, 745 }, 746 { 747 name: "both triggered jobs can start", 748 pjs: []prowapi.ProwJob{ 749 { 750 ObjectMeta: metav1.ObjectMeta{ 751 Name: "first", 752 Namespace: "prowjobs", 753 }, 754 Spec: prowapi.ProwJobSpec{ 755 Job: "test-bazel-build", 756 Type: prowapi.PostsubmitJob, 757 MaxConcurrency: 2, 758 }, 759 Status: prowapi.ProwJobStatus{ 760 State: prowapi.TriggeredState, 761 }, 762 }, 763 { 764 ObjectMeta: metav1.ObjectMeta{ 765 Name: "second", 766 Namespace: "prowjobs", 767 }, 768 Spec: prowapi.ProwJobSpec{ 769 Job: "test-bazel-build", 770 Type: prowapi.PostsubmitJob, 771 MaxConcurrency: 2, 772 }, 773 Status: prowapi.ProwJobStatus{ 774 State: prowapi.TriggeredState, 775 }, 776 }, 777 }, 778 pendingJobs: make(map[string]int), 779 expectedBuilds: 2, 780 }, 781 { 782 name: "no triggered job can start", 783 pjs: []prowapi.ProwJob{ 784 { 785 ObjectMeta: metav1.ObjectMeta{ 786 Name: "first", 787 Namespace: "prowjobs", 788 }, 789 Spec: prowapi.ProwJobSpec{ 790 Job: "test-bazel-build", 791 Type: prowapi.PostsubmitJob, 792 MaxConcurrency: 5, 793 }, 794 Status: prowapi.ProwJobStatus{ 795 State: prowapi.TriggeredState, 796 }, 797 }, 798 { 799 ObjectMeta: metav1.ObjectMeta{ 800 Name: "second", 801 Namespace: "prowjobs", 802 }, 803 Spec: prowapi.ProwJobSpec{ 804 Job: "test-bazel-build", 805 Type: prowapi.PostsubmitJob, 806 MaxConcurrency: 5, 807 }, 808 Status: prowapi.ProwJobStatus{ 809 State: prowapi.TriggeredState, 810 }, 811 }, 812 }, 813 pendingJobs: map[string]int{"test-bazel-build": 5}, 814 expectedBuilds: 0, 815 }, 816 } 817 818 for _, test := range tests { 819 totServ := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 820 fmt.Fprint(w, "42") 821 })) 822 defer totServ.Close() 823 jobs := make(chan prowapi.ProwJob, len(test.pjs)) 824 for _, pj := range test.pjs { 825 jobs <- pj 826 } 827 close(jobs) 828 829 var prowJobs []runtime.Object 830 for i := range test.pjs { 831 prowJobs = append(prowJobs, &test.pjs[i]) 832 } 833 fakeProwJobClient := fake.NewSimpleClientset(prowJobs...) 834 fjc := &fjc{} 835 c := Controller{ 836 prowJobClient: fakeProwJobClient.ProwV1().ProwJobs("prowjobs"), 837 jc: fjc, 838 log: logrus.NewEntry(logrus.StandardLogger()), 839 cfg: newFakeConfigAgent(t, 0, nil).Config, 840 totURL: totServ.URL, 841 pendingJobs: test.pendingJobs, 842 clock: clock.RealClock{}, 843 } 844 845 reports := make(chan<- prowapi.ProwJob, len(test.pjs)) 846 errors := make(chan<- error, len(test.pjs)) 847 848 syncProwJobs(c.log, c.syncTriggeredJob, 20, jobs, reports, errors, nil) 849 if len(fjc.pjs) != test.expectedBuilds { 850 t.Errorf("expected builds: %d, got: %d", test.expectedBuilds, len(fjc.pjs)) 851 } 852 } 853 } 854 855 func TestGetJenkinsJobs(t *testing.T) { 856 now := func() *metav1.Time { 857 n := metav1.Now() 858 return &n 859 } 860 tests := []struct { 861 name string 862 pjs []prowapi.ProwJob 863 expected []string 864 }{ 865 { 866 name: "both complete and running", 867 pjs: []prowapi.ProwJob{ 868 { 869 Spec: prowapi.ProwJobSpec{ 870 Job: "coolio", 871 }, 872 Status: prowapi.ProwJobStatus{ 873 CompletionTime: now(), 874 }, 875 }, 876 { 877 Spec: prowapi.ProwJobSpec{ 878 Job: "maradona", 879 }, 880 Status: prowapi.ProwJobStatus{}, 881 }, 882 }, 883 expected: []string{"maradona"}, 884 }, 885 { 886 name: "only complete", 887 pjs: []prowapi.ProwJob{ 888 { 889 Spec: prowapi.ProwJobSpec{ 890 Job: "coolio", 891 }, 892 Status: prowapi.ProwJobStatus{ 893 CompletionTime: now(), 894 }, 895 }, 896 { 897 Spec: prowapi.ProwJobSpec{ 898 Job: "maradona", 899 }, 900 Status: prowapi.ProwJobStatus{ 901 CompletionTime: now(), 902 }, 903 }, 904 }, 905 expected: nil, 906 }, 907 { 908 name: "only running", 909 pjs: []prowapi.ProwJob{ 910 { 911 Spec: prowapi.ProwJobSpec{ 912 Job: "coolio", 913 }, 914 Status: prowapi.ProwJobStatus{}, 915 }, 916 { 917 Spec: prowapi.ProwJobSpec{ 918 Job: "maradona", 919 }, 920 Status: prowapi.ProwJobStatus{}, 921 }, 922 }, 923 expected: []string{"maradona", "coolio"}, 924 }, 925 { 926 name: "running jenkins jobs", 927 pjs: []prowapi.ProwJob{ 928 { 929 Spec: prowapi.ProwJobSpec{ 930 Job: "coolio", 931 Agent: "jenkins", 932 JenkinsSpec: &prowapi.JenkinsSpec{ 933 GitHubBranchSourceJob: true, 934 }, 935 Refs: &prowapi.Refs{ 936 BaseRef: "master", 937 Pulls: []prowapi.Pull{{ 938 Number: 12, 939 }}, 940 }, 941 }, 942 Status: prowapi.ProwJobStatus{}, 943 }, 944 { 945 Spec: prowapi.ProwJobSpec{ 946 Job: "maradona", 947 Agent: "jenkins", 948 JenkinsSpec: &prowapi.JenkinsSpec{ 949 GitHubBranchSourceJob: true, 950 }, 951 Refs: &prowapi.Refs{ 952 BaseRef: "master", 953 }, 954 }, 955 Status: prowapi.ProwJobStatus{}, 956 }, 957 }, 958 expected: []string{"maradona/job/master", "coolio/view/change-requests/job/PR-12"}, 959 }, 960 } 961 962 for _, test := range tests { 963 t.Logf("scenario %q", test.name) 964 got := getJenkinsJobs(test.pjs) 965 if len(got) != len(test.expected) { 966 t.Errorf("unexpected job amount: %d (%v), expected: %d (%v)", 967 len(got), got, len(test.expected), test.expected) 968 } 969 for _, ej := range test.expected { 970 var found bool 971 for _, gj := range got { 972 if ej == gj.JobName { 973 found = true 974 break 975 } 976 } 977 if !found { 978 t.Errorf("expected jobs: %v\ngot: %v", test.expected, got) 979 } 980 } 981 } 982 } 983 984 func TestOperatorConfig(t *testing.T) { 985 tests := []struct { 986 name string 987 988 operators []config.JenkinsOperator 989 labelSelector string 990 991 expected config.Controller 992 }{ 993 { 994 name: "single operator config", 995 996 operators: nil, // defaults to a single operator 997 labelSelector: "", 998 999 expected: config.Controller{ 1000 JobURLTemplate: template.Must(template.New("test").Parse("{{.Status.PodName}}/{{.Status.State}}")), 1001 MaxConcurrency: 10, 1002 MaxGoroutines: 20, 1003 }, 1004 }, 1005 { 1006 name: "single operator config, --label-selector used", 1007 1008 operators: nil, // defaults to a single operator 1009 labelSelector: "master=ci.jenkins.org", 1010 1011 expected: config.Controller{ 1012 JobURLTemplate: template.Must(template.New("test").Parse("{{.Status.PodName}}/{{.Status.State}}")), 1013 MaxConcurrency: 10, 1014 MaxGoroutines: 20, 1015 }, 1016 }, 1017 { 1018 name: "multiple operator config", 1019 1020 operators: []config.JenkinsOperator{ 1021 { 1022 Controller: config.Controller{ 1023 JobURLTemplate: template.Must(template.New("test").Parse("{{.Status.PodName}}/{{.Status.State}}")), 1024 MaxConcurrency: 5, 1025 MaxGoroutines: 10, 1026 }, 1027 LabelSelectorString: "master=ci.openshift.org", 1028 }, 1029 { 1030 Controller: config.Controller{ 1031 MaxConcurrency: 100, 1032 MaxGoroutines: 100, 1033 }, 1034 LabelSelectorString: "master=ci.jenkins.org", 1035 }, 1036 }, 1037 labelSelector: "master=ci.jenkins.org", 1038 1039 expected: config.Controller{ 1040 MaxConcurrency: 100, 1041 MaxGoroutines: 100, 1042 }, 1043 }, 1044 } 1045 1046 for _, test := range tests { 1047 t.Logf("scenario %q", test.name) 1048 1049 c := Controller{ 1050 cfg: newFakeConfigAgent(t, 10, test.operators).Config, 1051 selector: test.labelSelector, 1052 clock: clock.RealClock{}, 1053 } 1054 1055 got := c.config() 1056 if !reflect.DeepEqual(got, test.expected) { 1057 t.Errorf("expected controller:\n%#v\ngot controller:\n%#v\n", test.expected, got) 1058 } 1059 } 1060 } 1061 1062 func TestSyncAbortedJob(t *testing.T) { 1063 testCases := []struct { 1064 name string 1065 hasBuild bool 1066 abortErrors bool 1067 expectAbort bool 1068 expectComplete bool 1069 }{ 1070 { 1071 name: "Build is aborted", 1072 hasBuild: true, 1073 expectAbort: true, 1074 expectComplete: true, 1075 }, 1076 { 1077 name: "No build, no abort", 1078 hasBuild: false, 1079 expectAbort: false, 1080 expectComplete: true, 1081 }, 1082 { 1083 name: "Abort errors, job is not marked completed", 1084 hasBuild: true, 1085 abortErrors: true, 1086 expectComplete: false, 1087 }, 1088 } 1089 1090 for _, tc := range testCases { 1091 t.Run(tc.name, func(t *testing.T) { 1092 1093 pj := &prowapi.ProwJob{ 1094 ObjectMeta: metav1.ObjectMeta{ 1095 Name: "my-pj", 1096 }, 1097 Status: prowapi.ProwJobStatus{ 1098 State: prowapi.AbortedState, 1099 }, 1100 } 1101 1102 var buildMap map[string]Build 1103 if tc.hasBuild { 1104 buildMap = map[string]Build{pj.Name: {}} 1105 } 1106 pjClient := fake.NewSimpleClientset(pj) 1107 jobClient := &fjc{abortErrors: tc.abortErrors} 1108 c := &Controller{ 1109 log: logrus.NewEntry(logrus.New()), 1110 prowJobClient: pjClient.ProwV1().ProwJobs(""), 1111 jc: jobClient, 1112 } 1113 1114 if err := c.syncAbortedJob(*pj, nil, buildMap); (err != nil) != tc.abortErrors { 1115 t.Fatalf("syncAbortedJob failed: %v", err) 1116 } 1117 1118 pj, err := pjClient.ProwV1().ProwJobs("").Get(context.Background(), pj.Name, metav1.GetOptions{}) 1119 if err != nil { 1120 t.Fatalf("failed to get prowjob: %v", err) 1121 } 1122 1123 if pj.Complete() != tc.expectComplete { 1124 t.Errorf("expected completed job: %t, got completed job: %t", tc.expectComplete, pj.Complete()) 1125 } 1126 1127 if jobClient.didAbort != tc.expectAbort { 1128 t.Errorf("expected abort: %t, did abort: %t", tc.expectAbort, jobClient.didAbort) 1129 } 1130 }) 1131 } 1132 }