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