Skip to content

Commit 41560c1

Browse files
CasLubbersotomi-adminsvcAPLBot
authored
feat: add support for deleting Tekton-managed pods and enhance Istio … (#2287)
Co-authored-by: otomi-admin <[email protected]> Co-authored-by: svcAPLBot <[email protected]>
1 parent 64defcc commit 41560c1

File tree

2 files changed

+260
-3
lines changed

2 files changed

+260
-3
lines changed

src/common/runtime-upgrades/restart-istio-sidecars.test.ts

Lines changed: 221 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { CoreV1Api, V1OwnerReference } from '@kubernetes/client-node'
22
import {
3+
deletePod,
34
detectAndRestartOutdatedIstioSidecars,
45
getDeploymentNameFromReplicaSet,
56
getIstioVersionFromDeployment,
7+
isPodManagedByTekton,
68
restartCluster,
79
restartDeployment,
810
restartPodOwner,
@@ -474,6 +476,164 @@ describe('restartDeployment', () => {
474476
})
475477
})
476478

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+
477637
describe('restartPodOwner', () => {
478638
let mockRestartedDeployments: Set<string>
479639
let mockD: any
@@ -567,21 +727,80 @@ describe('restartPodOwner', () => {
567727
expect(mockD.info).toHaveBeenCalledWith('Restarting deployment test-deployment in namespace test-namespace')
568728
})
569729

570-
it('should handle pods with no owner references', async () => {
730+
it('should warn about standalone pods with no owner references', async () => {
571731
const mockParsedArgs = { dryRun: false, local: false }
572732

573733
mockPod = {
574734
metadata: {
575735
namespace: 'test-namespace',
736+
name: 'standalone-pod',
576737
},
577738
}
578739

579740
await restartPodOwner(mockPod, mockD, mockParsedArgs)
580741

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+
)
582745
expect(mockD.info).not.toHaveBeenCalled()
583746
})
584747

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+
585804
it('should handle pods with no metadata', async () => {
586805
const mockParsedArgs = { dryRun: false, local: false }
587806

src/common/runtime-upgrades/restart-istio-sidecars.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,46 @@ export async function restartDeployment(
235235
return deploymentName
236236
}
237237

238+
export function isPodManagedByTekton(pod: V1Pod): boolean {
239+
return (
240+
pod.metadata?.labels?.['app.kubernetes.io/managed-by'] === 'EventListener' ||
241+
pod.metadata?.labels?.['eventlistener'] !== undefined ||
242+
pod.metadata?.labels?.['app.kubernetes.io/part-of'] === 'Triggers'
243+
)
244+
}
245+
246+
export async function deletePod(pod: V1Pod, d: OtomiDebugger, parsedArgs: any): Promise<void> {
247+
if (!pod.metadata?.name || !pod.metadata?.namespace) return
248+
249+
if (!parsedArgs?.dryRun && !parsedArgs?.local) {
250+
d.info(`Deleting pod ${pod.metadata.namespace}/${pod.metadata.name} to refresh Istio sidecar`)
251+
const coreApi = k8s.core()
252+
await coreApi.deleteNamespacedPod({
253+
name: pod.metadata.name,
254+
namespace: pod.metadata.namespace,
255+
})
256+
d.info(`Successfully deleted pod ${pod.metadata.name}`)
257+
} else {
258+
d.info(`Dry run mode - would delete pod ${pod.metadata.namespace}/${pod.metadata.name}`)
259+
}
260+
}
261+
238262
export async function restartPodOwner(pod: V1Pod, d: OtomiDebugger, parsedArgs: any): Promise<void> {
239-
if (!pod.metadata?.ownerReferences || !pod.metadata?.namespace) return
263+
if (!pod.metadata?.namespace) return
264+
265+
// Handle pods without owner references (standalone pods)
266+
if (!pod.metadata?.ownerReferences || pod.metadata.ownerReferences.length === 0) {
267+
d.warn(
268+
`Pod ${pod.metadata.namespace}/${pod.metadata.name} has no owner references (standalone pod). Cannot automatically restart - manual intervention required to update Istio sidecar.`,
269+
)
270+
return
271+
}
272+
273+
// For Tekton-managed pods, delete the pod instead of restarting the deployment
274+
if (isPodManagedByTekton(pod)) {
275+
await deletePod(pod, d, parsedArgs)
276+
return
277+
}
240278

241279
for (const ownerRef of pod.metadata.ownerReferences) {
242280
if (!ownerRef.name) continue

0 commit comments

Comments
 (0)