|
1 | 1 | import { CoreV1Api, V1OwnerReference } from '@kubernetes/client-node'
|
2 | 2 | import {
|
| 3 | + deletePod, |
3 | 4 | detectAndRestartOutdatedIstioSidecars,
|
4 | 5 | getDeploymentNameFromReplicaSet,
|
5 | 6 | getIstioVersionFromDeployment,
|
| 7 | + isPodManagedByTekton, |
6 | 8 | restartCluster,
|
7 | 9 | restartDeployment,
|
8 | 10 | restartPodOwner,
|
@@ -474,6 +476,164 @@ describe('restartDeployment', () => {
|
474 | 476 | })
|
475 | 477 | })
|
476 | 478 |
|
| 479 | +describe('isPodManagedByTekton', () => { |
| 480 | + it('should return true for pods managed by EventListener', () => { |
| 481 | + const pod = { |
| 482 | + metadata: { |
| 483 | + labels: { |
| 484 | + 'app.kubernetes.io/managed-by': 'EventListener', |
| 485 | + }, |
| 486 | + }, |
| 487 | + } |
| 488 | + |
| 489 | + expect(isPodManagedByTekton(pod)).toBe(true) |
| 490 | + }) |
| 491 | + |
| 492 | + it('should return true for pods with eventlistener label', () => { |
| 493 | + const pod = { |
| 494 | + metadata: { |
| 495 | + labels: { |
| 496 | + eventlistener: 'gitea-webhook-main', |
| 497 | + }, |
| 498 | + }, |
| 499 | + } |
| 500 | + |
| 501 | + expect(isPodManagedByTekton(pod)).toBe(true) |
| 502 | + }) |
| 503 | + |
| 504 | + it('should return true for pods part of Triggers', () => { |
| 505 | + const pod = { |
| 506 | + metadata: { |
| 507 | + labels: { |
| 508 | + 'app.kubernetes.io/part-of': 'Triggers', |
| 509 | + }, |
| 510 | + }, |
| 511 | + } |
| 512 | + |
| 513 | + expect(isPodManagedByTekton(pod)).toBe(true) |
| 514 | + }) |
| 515 | + |
| 516 | + it('should return false for pods not managed by Tekton', () => { |
| 517 | + const pod = { |
| 518 | + metadata: { |
| 519 | + labels: { |
| 520 | + 'app.kubernetes.io/managed-by': 'Deployment', |
| 521 | + 'app.kubernetes.io/part-of': 'SomeOtherApp', |
| 522 | + }, |
| 523 | + }, |
| 524 | + } |
| 525 | + |
| 526 | + expect(isPodManagedByTekton(pod)).toBe(false) |
| 527 | + }) |
| 528 | + |
| 529 | + it('should return false for pods without labels', () => { |
| 530 | + const pod = { |
| 531 | + metadata: {}, |
| 532 | + } |
| 533 | + |
| 534 | + expect(isPodManagedByTekton(pod)).toBe(false) |
| 535 | + }) |
| 536 | + |
| 537 | + it('should return false for pods without metadata', () => { |
| 538 | + const pod = {} |
| 539 | + |
| 540 | + expect(isPodManagedByTekton(pod)).toBe(false) |
| 541 | + }) |
| 542 | +}) |
| 543 | + |
| 544 | +describe('deletePod', () => { |
| 545 | + let mockD: any |
| 546 | + let mockCoreApi: any |
| 547 | + |
| 548 | + beforeEach(() => { |
| 549 | + mockD = { |
| 550 | + info: jest.fn(), |
| 551 | + } |
| 552 | + mockCoreApi = { |
| 553 | + deleteNamespacedPod: jest.fn().mockResolvedValue({}), |
| 554 | + } |
| 555 | + ;(k8s.core as jest.Mock).mockReturnValue(mockCoreApi) |
| 556 | + jest.clearAllMocks() |
| 557 | + }) |
| 558 | + |
| 559 | + it('should delete pod when not in dry run mode', async () => { |
| 560 | + const mockParsedArgs = { dryRun: false, local: false } |
| 561 | + const pod = { |
| 562 | + metadata: { |
| 563 | + name: 'test-pod', |
| 564 | + namespace: 'test-namespace', |
| 565 | + }, |
| 566 | + } |
| 567 | + |
| 568 | + await deletePod(pod, mockD, mockParsedArgs) |
| 569 | + |
| 570 | + expect(mockD.info).toHaveBeenCalledWith('Deleting pod test-namespace/test-pod to refresh Istio sidecar') |
| 571 | + expect(mockD.info).toHaveBeenCalledWith('Successfully deleted pod test-pod') |
| 572 | + expect(mockCoreApi.deleteNamespacedPod).toHaveBeenCalledWith({ |
| 573 | + name: 'test-pod', |
| 574 | + namespace: 'test-namespace', |
| 575 | + }) |
| 576 | + }) |
| 577 | + |
| 578 | + it('should not delete pod in dry run mode', async () => { |
| 579 | + const mockParsedArgs = { dryRun: true, local: false } |
| 580 | + const pod = { |
| 581 | + metadata: { |
| 582 | + name: 'test-pod', |
| 583 | + namespace: 'test-namespace', |
| 584 | + }, |
| 585 | + } |
| 586 | + |
| 587 | + await deletePod(pod, mockD, mockParsedArgs) |
| 588 | + |
| 589 | + expect(mockD.info).toHaveBeenCalledWith('Dry run mode - would delete pod test-namespace/test-pod') |
| 590 | + expect(mockCoreApi.deleteNamespacedPod).not.toHaveBeenCalled() |
| 591 | + }) |
| 592 | + |
| 593 | + it('should not delete pod in local mode', async () => { |
| 594 | + const mockParsedArgs = { dryRun: false, local: true } |
| 595 | + const pod = { |
| 596 | + metadata: { |
| 597 | + name: 'test-pod', |
| 598 | + namespace: 'test-namespace', |
| 599 | + }, |
| 600 | + } |
| 601 | + |
| 602 | + await deletePod(pod, mockD, mockParsedArgs) |
| 603 | + |
| 604 | + expect(mockD.info).toHaveBeenCalledWith('Dry run mode - would delete pod test-namespace/test-pod') |
| 605 | + expect(mockCoreApi.deleteNamespacedPod).not.toHaveBeenCalled() |
| 606 | + }) |
| 607 | + |
| 608 | + it('should handle pods without name gracefully', async () => { |
| 609 | + const mockParsedArgs = { dryRun: false, local: false } |
| 610 | + const pod = { |
| 611 | + metadata: { |
| 612 | + namespace: 'test-namespace', |
| 613 | + }, |
| 614 | + } |
| 615 | + |
| 616 | + await deletePod(pod, mockD, mockParsedArgs) |
| 617 | + |
| 618 | + expect(mockD.info).not.toHaveBeenCalled() |
| 619 | + expect(mockCoreApi.deleteNamespacedPod).not.toHaveBeenCalled() |
| 620 | + }) |
| 621 | + |
| 622 | + it('should handle pods without namespace gracefully', async () => { |
| 623 | + const mockParsedArgs = { dryRun: false, local: false } |
| 624 | + const pod = { |
| 625 | + metadata: { |
| 626 | + name: 'test-pod', |
| 627 | + }, |
| 628 | + } |
| 629 | + |
| 630 | + await deletePod(pod, mockD, mockParsedArgs) |
| 631 | + |
| 632 | + expect(mockD.info).not.toHaveBeenCalled() |
| 633 | + expect(mockCoreApi.deleteNamespacedPod).not.toHaveBeenCalled() |
| 634 | + }) |
| 635 | +}) |
| 636 | + |
477 | 637 | describe('restartPodOwner', () => {
|
478 | 638 | let mockRestartedDeployments: Set<string>
|
479 | 639 | let mockD: any
|
@@ -567,21 +727,80 @@ describe('restartPodOwner', () => {
|
567 | 727 | expect(mockD.info).toHaveBeenCalledWith('Restarting deployment test-deployment in namespace test-namespace')
|
568 | 728 | })
|
569 | 729 |
|
570 |
| - it('should handle pods with no owner references', async () => { |
| 730 | + it('should warn about standalone pods with no owner references', async () => { |
571 | 731 | const mockParsedArgs = { dryRun: false, local: false }
|
572 | 732 |
|
573 | 733 | mockPod = {
|
574 | 734 | metadata: {
|
575 | 735 | namespace: 'test-namespace',
|
| 736 | + name: 'standalone-pod', |
576 | 737 | },
|
577 | 738 | }
|
578 | 739 |
|
579 | 740 | await restartPodOwner(mockPod, mockD, mockParsedArgs)
|
580 | 741 |
|
581 |
| - // No restart attempted since deployment name extraction failed |
| 742 | + expect(mockD.warn).toHaveBeenCalledWith( |
| 743 | + 'Pod test-namespace/standalone-pod has no owner references (standalone pod). Cannot automatically restart - manual intervention required to update Istio sidecar.', |
| 744 | + ) |
582 | 745 | expect(mockD.info).not.toHaveBeenCalled()
|
583 | 746 | })
|
584 | 747 |
|
| 748 | + it('should warn about standalone pods with empty owner references', async () => { |
| 749 | + const mockParsedArgs = { dryRun: false, local: false } |
| 750 | + |
| 751 | + mockPod = { |
| 752 | + metadata: { |
| 753 | + namespace: 'test-namespace', |
| 754 | + name: 'standalone-pod', |
| 755 | + ownerReferences: [], |
| 756 | + }, |
| 757 | + } |
| 758 | + |
| 759 | + await restartPodOwner(mockPod, mockD, mockParsedArgs) |
| 760 | + |
| 761 | + expect(mockD.warn).toHaveBeenCalledWith( |
| 762 | + 'Pod test-namespace/standalone-pod has no owner references (standalone pod). Cannot automatically restart - manual intervention required to update Istio sidecar.', |
| 763 | + ) |
| 764 | + expect(mockD.info).not.toHaveBeenCalled() |
| 765 | + }) |
| 766 | + |
| 767 | + it('should delete Tekton-managed pods instead of restarting deployment', async () => { |
| 768 | + const mockParsedArgs = { dryRun: false, local: false } |
| 769 | + const mockCoreApi = { |
| 770 | + deleteNamespacedPod: jest.fn().mockResolvedValue({}), |
| 771 | + } |
| 772 | + ;(k8s.core as jest.Mock).mockReturnValue(mockCoreApi) |
| 773 | + |
| 774 | + mockPod = { |
| 775 | + metadata: { |
| 776 | + namespace: 'team-labs', |
| 777 | + name: 'el-gitea-webhook-main-abc123', |
| 778 | + labels: { |
| 779 | + 'app.kubernetes.io/managed-by': 'EventListener', |
| 780 | + eventlistener: 'gitea-webhook-main', |
| 781 | + }, |
| 782 | + ownerReferences: [ |
| 783 | + { |
| 784 | + kind: 'ReplicaSet', |
| 785 | + name: 'el-gitea-webhook-main-abc123', |
| 786 | + apiVersion: 'apps/v1', |
| 787 | + uid: 'test-uid', |
| 788 | + }, |
| 789 | + ], |
| 790 | + }, |
| 791 | + } |
| 792 | + |
| 793 | + await restartPodOwner(mockPod, mockD, mockParsedArgs) |
| 794 | + |
| 795 | + expect(mockD.info).toHaveBeenCalledWith( |
| 796 | + 'Deleting pod team-labs/el-gitea-webhook-main-abc123 to refresh Istio sidecar', |
| 797 | + ) |
| 798 | + expect(mockCoreApi.deleteNamespacedPod).toHaveBeenCalledWith({ |
| 799 | + name: 'el-gitea-webhook-main-abc123', |
| 800 | + namespace: 'team-labs', |
| 801 | + }) |
| 802 | + }) |
| 803 | + |
585 | 804 | it('should handle pods with no metadata', async () => {
|
586 | 805 | const mockParsedArgs = { dryRun: false, local: false }
|
587 | 806 |
|
|
0 commit comments