From b9a5b69de6d8486ee5499d9b35b531fb63789691 Mon Sep 17 00:00:00 2001 From: biersoeckli Date: Thu, 14 Nov 2024 11:53:01 +0000 Subject: [PATCH] created logic to delete all k8s items when app deleted --- src/app/api/pod-logs/route.ts | 3 +- .../project/app/[tabName]/overview/actions.ts | 3 +- src/server/services/app.service.ts | 18 +++-- src/server/services/build.service.ts | 19 +++++ src/server/services/deployment.service.ts | 79 ++----------------- src/server/services/ingress.service.ts | 14 +++- src/server/services/pod.service.ts | 19 +++++ src/server/services/project.service.ts | 8 +- src/server/services/pvc.service.ts | 9 +++ src/server/services/svc.service.ts | 72 +++++++++++++++++ 10 files changed, 155 insertions(+), 89 deletions(-) create mode 100644 src/server/services/svc.service.ts diff --git a/src/app/api/pod-logs/route.ts b/src/app/api/pod-logs/route.ts index 7b583f5..ea2ecfe 100644 --- a/src/app/api/pod-logs/route.ts +++ b/src/app/api/pod-logs/route.ts @@ -24,7 +24,7 @@ export async function POST(request: Request) { let pod; let streamKey; if (namespace && podName) { - pod = await deploymentService.getPodByName(namespace, podName); + pod = await podService.getPodInfoByName(namespace, podName); streamKey = `${namespace}_${podName}`; } else if (buildJobName) { @@ -36,7 +36,6 @@ export async function POST(request: Request) { console.error('Invalid pod info for streaming logs', podInfo); return new Response("Invalid pod info", { status: 400 }); } - console.log('pod', pod) let k3sStreamRequest: any | undefined; let logStream: stream.PassThrough | undefined; diff --git a/src/app/project/app/[tabName]/overview/actions.ts b/src/app/project/app/[tabName]/overview/actions.ts index fb3ba5a..879a5e6 100644 --- a/src/app/project/app/[tabName]/overview/actions.ts +++ b/src/app/project/app/[tabName]/overview/actions.ts @@ -7,6 +7,7 @@ import { ServerActionResult, SuccessActionResult } from "@/model/server-action-e import appService from "@/server/services/app.service"; import buildService from "@/server/services/build.service"; import deploymentService from "@/server/services/deployment.service"; +import podService from "@/server/services/pod.service"; import { getAuthUserSession, simpleAction } from "@/server/utils/action-wrapper.utils"; @@ -28,5 +29,5 @@ export const getPodsForApp = async (appId: string) => simpleAction(async () => { await getAuthUserSession(); const app = await appService.getExtendedById(appId); - return await deploymentService.getPodsForApp(app.projectId, appId); + return await podService.getPodsForApp(app.projectId, appId); }) as Promise>; \ No newline at end of file diff --git a/src/server/services/app.service.ts b/src/server/services/app.service.ts index f939c9f..09c6bcd 100644 --- a/src/server/services/app.service.ts +++ b/src/server/services/app.service.ts @@ -9,6 +9,9 @@ import { StringUtils } from "../utils/string.utils"; import deploymentService from "./deployment.service"; import buildService, { BUILD_NAMESPACE } from "./build.service"; import namespaceService from "./namespace.service"; +import ingressService from "./ingress.service"; +import pvcService from "./pvc.service"; +import svcService from "./svc.service"; class AppService { @@ -29,21 +32,24 @@ class AppService { } async deleteById(id: string) { - const existingItem = await this.getById(id); - if (!existingItem) { + const existingApp = await this.getById(id); + if (!existingApp) { return; } try { - await deploymentService.deleteService(existingItem.projectId, existingItem.id); - await deploymentService.deleteDeployment(existingItem.projectId, existingItem.id); + await svcService.deleteService(existingApp.projectId, existingApp.id); + await deploymentService.deleteDeployment(existingApp.projectId, existingApp.id); + await ingressService.deleteAllIngressForApp(existingApp.projectId, existingApp.id); + await pvcService.deleteAllPvcOfApp(existingApp.projectId, existingApp.id); + await buildService.deleteAllBuildsOfApp(existingApp.id); await dataAccess.client.app.delete({ where: { id } }); } finally { - revalidateTag(Tags.apps(existingItem.projectId)); - revalidateTag(Tags.app(existingItem.id)); + revalidateTag(Tags.apps(existingApp.projectId)); + revalidateTag(Tags.app(existingApp.id)); } } diff --git a/src/server/services/build.service.ts b/src/server/services/build.service.ts index e2f7ad1..0f1cda3 100644 --- a/src/server/services/build.service.ts +++ b/src/server/services/build.service.ts @@ -96,8 +96,27 @@ class BuildService { return `${REGISTRY_URL_EXTERNAL}/${appId}:latest`; } + + async deleteAllBuildsOfApp(appId: string) { + const jobNamePrefix = StringUtils.toJobName(appId); + const jobs = await k3s.batch.listNamespacedJob(BUILD_NAMESPACE); + const jobsOfBuild = jobs.body.items.filter((job) => job.metadata?.name?.startsWith(jobNamePrefix)); + for (const job of jobsOfBuild) { + await this.deleteBuild(job.metadata?.name!); + } + } + + async deleteAllBuildsOfProject(projectId: string) { + const jobs = await k3s.batch.listNamespacedJob(BUILD_NAMESPACE); + const jobsOfProject = jobs.body.items.filter((job) => job.metadata?.annotations?.[Constants.QS_ANNOTATION_PROJECT_ID] === projectId); + for (const job of jobsOfProject) { + await this.deleteBuild(job.metadata?.name!); + } + } + async deleteBuild(buildName: string) { await k3s.batch.deleteNamespacedJob(buildName, BUILD_NAMESPACE); + console.log(`Deleted build job ${buildName}`); } async getBuildsForApp(appId: string) { diff --git a/src/server/services/deployment.service.ts b/src/server/services/deployment.service.ts index b8e3abb..0389cb6 100644 --- a/src/server/services/deployment.service.ts +++ b/src/server/services/deployment.service.ts @@ -12,6 +12,7 @@ import pvcService from "./pvc.service"; import ingressService from "./ingress.service"; import namespaceService from "./namespace.service"; import { Constants } from "../utils/constants"; +import svcService from "./svc.service"; class DeploymentService { @@ -28,64 +29,9 @@ class DeploymentService { if (!existingDeployment) { return; } - return k3s.apps.deleteNamespacedDeployment(appId, projectId); - } - - async deleteService(projectId: string, appId: string) { - const existingService = await this.getService(projectId, appId); - if (!existingService) { - return; - } - return k3s.core.deleteNamespacedService(StringUtils.toServiceName(appId), projectId); - } - - - async getService(projectId: string, appId: string) { - const allServices = await k3s.core.listNamespacedService(projectId); - if (allServices.body.items.some((item) => item.metadata?.name === StringUtils.toServiceName(appId))) { - const res = await k3s.core.readNamespacedService(StringUtils.toServiceName(appId), projectId); - return res.body; - } - } - - async createOrUpdateService(app: AppExtendedModel) { - const existingService = await this.getService(app.projectId, app.id); - // port configuration with removed duplicates - const ports: { - name: string; - port: number; - targetPort: number; - }[] = [ - ...app.appDomains.map((domain) => ({ - name: `custom-${domain.id}`, - port: domain.port, - targetPort: domain.port - })), - { - name: 'default', - port: app.defaultPort, - targetPort: app.defaultPort - } - ].filter((port, index, self) => - index === self.findIndex((t) => (t.port === port.port && t.targetPort === port.targetPort))); - - const body = { - metadata: { - name: StringUtils.toServiceName(app.id) - }, - spec: { - selector: { - app: app.id - }, - ports: ports - } - }; - if (existingService) { - await k3s.core.replaceNamespacedService(StringUtils.toServiceName(app.id), app.projectId, body); - } else { - await k3s.core.createNamespacedService(app.projectId, body); - } - + const returnVal = await k3s.apps.deleteNamespacedDeployment(appId, projectId); + console.log(`Deleted Deployment ${appId} in namespace ${projectId}`); + return returnVal; } async validateDeployment(app: AppExtendedModel) { @@ -169,7 +115,7 @@ class DeploymentService { const res = await k3s.apps.createNamespacedDeployment(app.projectId, body); } await pvcService.deleteUnusedPvcOfApp(app); - await this.createOrUpdateService(app); + await svcService.createOrUpdateService(app); await ingressService.createOrUpdateIngressForApp(app); } @@ -190,13 +136,6 @@ class DeploymentService { return k3s.apps.replaceNamespacedDeployment(appId, projectId, existingDeployment); } - async getPodsForApp(projectId: string, appId: string) { - const res = await k3s.core.listNamespacedPod(projectId, undefined, undefined, undefined, undefined, `app=${appId}`); - return res.body.items.map((item) => ({ - podName: item.metadata?.name!, - containerName: item.spec?.containers?.[0].name! - })).filter((item) => !!item.podName && !!item.containerName) as PodsInfoModel[]; - } async getDeploymentStatus(projectId: string, appId: string) { const deployment = await this.getDeployment(projectId, appId); @@ -206,14 +145,6 @@ class DeploymentService { return this.mapReplicasetToStatus(deployment); } - async getPodByName(projectId: string, podName: string) { - const res = await k3s.core.readNamespacedPod(podName, projectId); - return { - podName: res.body.metadata?.name!, - containerName: res.body.spec?.containers?.[0].name! - } as PodsInfoModel; - } - /** * Searches for Build Jobs (only for Git Projects) and ReplicaSets (for all projects) and returns a list of DeploymentModel * Build are only included if they are in status RUNNING, FAILED or UNKNOWN. SUCCESSFUL builds are not included because they are already part of the ReplicaSet history. diff --git a/src/server/services/ingress.service.ts b/src/server/services/ingress.service.ts index 67767c2..51fcabd 100644 --- a/src/server/services/ingress.service.ts +++ b/src/server/services/ingress.service.ts @@ -2,7 +2,7 @@ import { AppExtendedModel } from "@/model/app-extended.model"; import k3s from "../adapter/kubernetes-api.adapter"; import { V1Ingress } from "@kubernetes/client-node"; import { StringUtils } from "../utils/string.utils"; -import { AppDomain } from "@prisma/client"; +import { App, AppDomain } from "@prisma/client"; import { Constants } from "../utils/constants"; @@ -21,7 +21,7 @@ class IngressService { } - async deleteObsoleteIngresses(app: AppExtendedModel) { + async deleteUnusedIngressesOfApp(app: AppExtendedModel) { const currentDomains = new Set(app.appDomains.map(domainObj => domainObj.hostname)); const existingIngresses = await this.getAllIngressForApp(app.projectId, app.id); @@ -42,6 +42,14 @@ class IngressService { } } + async deleteAllIngressForApp(projectId: string, appId: string) { + const existingIngresses = await this.getAllIngressForApp(projectId, appId); + for (const ingress of existingIngresses) { + await k3s.network.deleteNamespacedIngress(ingress.metadata!.name!, projectId); + console.log(`Deleted Ingress ${ingress.metadata!.name} for app ${appId}`); + } + } + async createOrUpdateIngressForApp(app: AppExtendedModel) { @@ -51,7 +59,7 @@ class IngressService { await this.createIngress(app, domainObj); } - await this.deleteObsoleteIngresses(app); + await this.deleteUnusedIngressesOfApp(app); } async createIngress(app: AppExtendedModel, domain: AppDomain) { diff --git a/src/server/services/pod.service.ts b/src/server/services/pod.service.ts index af10364..8144032 100644 --- a/src/server/services/pod.service.ts +++ b/src/server/services/pod.service.ts @@ -1,3 +1,4 @@ +import { PodsInfoModel } from "@/model/pods-info.model"; import k3s from "../adapter/kubernetes-api.adapter"; import { ServiceException } from "@/model/service.exception.model"; @@ -27,6 +28,24 @@ class PodService { const res = await k3s.core.readNamespacedPod(podName, projectId); return res.body; } + + + async getPodInfoByName(projectId: string, podName: string) { + const res = await k3s.core.readNamespacedPod(podName, projectId); + return { + podName: res.body.metadata?.name!, + containerName: res.body.spec?.containers?.[0].name! + } as PodsInfoModel; + } + + + async getPodsForApp(projectId: string, appId: string) { + const res = await k3s.core.listNamespacedPod(projectId, undefined, undefined, undefined, undefined, `app=${appId}`); + return res.body.items.map((item) => ({ + podName: item.metadata?.name!, + containerName: item.spec?.containers?.[0].name! + })).filter((item) => !!item.podName && !!item.containerName) as PodsInfoModel[]; + } } const podService = new PodService(); diff --git a/src/server/services/project.service.ts b/src/server/services/project.service.ts index f59d8ae..74d0fc1 100644 --- a/src/server/services/project.service.ts +++ b/src/server/services/project.service.ts @@ -5,19 +5,21 @@ import { Prisma, Project } from "@prisma/client"; import { StringUtils } from "../utils/string.utils"; import deploymentService from "./deployment.service"; import namespaceService from "./namespace.service"; +import buildService from "./build.service"; class ProjectService { - async deleteById(id: string) { - const existingItem = await this.getById(id); + async deleteById(projectid: string) { + const existingItem = await this.getById(projectid); if (!existingItem) { return; } try { + await buildService.deleteAllBuildsOfProject(existingItem.id); await namespaceService.deleteNamespace(existingItem.id); await dataAccess.client.project.delete({ where: { - id + id: projectid } }); } finally { diff --git a/src/server/services/pvc.service.ts b/src/server/services/pvc.service.ts index 67b4df8..2160aab 100644 --- a/src/server/services/pvc.service.ts +++ b/src/server/services/pvc.service.ts @@ -40,6 +40,15 @@ class PvcService { } } + async deleteAllPvcOfApp(projectId: string, appId: string) { + const existingPvc = await this.getAllPvcForApp(projectId, appId); + + for (const pvc of existingPvc) { + await k3s.core.deleteNamespacedPersistentVolumeClaim(pvc.metadata!.name!, projectId); + console.log(`Deleted PVC ${pvc.metadata!.name!} for app ${appId}`); + } + } + async createOrUpdatePvc(app: AppExtendedModel) { const existingPvcs = await this.getAllPvcForApp(app.projectId, app.id); diff --git a/src/server/services/svc.service.ts b/src/server/services/svc.service.ts new file mode 100644 index 0000000..1a1290a --- /dev/null +++ b/src/server/services/svc.service.ts @@ -0,0 +1,72 @@ +import { AppExtendedModel } from "@/model/app-extended.model"; +import k3s from "../adapter/kubernetes-api.adapter"; +import { V1PersistentVolumeClaim } from "@kubernetes/client-node"; +import { ServiceException } from "@/model/service.exception.model"; +import { AppVolume } from "@prisma/client"; +import { StringUtils } from "../utils/string.utils"; +import { Constants } from "../utils/constants"; + +class SvcService { + + async deleteService(projectId: string, appId: string) { + const existingService = await this.getService(projectId, appId); + if (!existingService) { + return; + } + const returnVal = await k3s.core.deleteNamespacedService(StringUtils.toServiceName(appId), projectId); + console.log(`Deleted Service ${StringUtils.toServiceName(appId)} in namespace ${projectId}`); + return returnVal; + } + + + async getService(projectId: string, appId: string) { + const allServices = await k3s.core.listNamespacedService(projectId); + if (allServices.body.items.some((item) => item.metadata?.name === StringUtils.toServiceName(appId))) { + const res = await k3s.core.readNamespacedService(StringUtils.toServiceName(appId), projectId); + return res.body; + } + } + + async createOrUpdateService(app: AppExtendedModel) { + const existingService = await this.getService(app.projectId, app.id); + // port configuration with removed duplicates + const ports: { + name: string; + port: number; + targetPort: number; + }[] = [ + ...app.appDomains.map((domain) => ({ + name: `custom-${domain.id}`, + port: domain.port, + targetPort: domain.port + })), + { + name: 'default', + port: app.defaultPort, + targetPort: app.defaultPort + } + ].filter((port, index, self) => + index === self.findIndex((t) => (t.port === port.port && t.targetPort === port.targetPort))); + + const body = { + metadata: { + name: StringUtils.toServiceName(app.id) + }, + spec: { + selector: { + app: app.id + }, + ports: ports + } + }; + if (existingService) { + await k3s.core.replaceNamespacedService(StringUtils.toServiceName(app.id), app.projectId, body); + } else { + await k3s.core.createNamespacedService(app.projectId, body); + } + + } +} + +const svcService = new SvcService(); +export default svcService;