created logic to delete all k8s items when app deleted

This commit is contained in:
biersoeckli
2024-11-14 11:53:01 +00:00
parent 79bee55186
commit b9a5b69de6
10 changed files with 155 additions and 89 deletions

View File

@@ -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;

View File

@@ -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<ServerActionResult<unknown, PodsInfoModel[]>>;

View File

@@ -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));
}
}

View File

@@ -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) {

View File

@@ -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.

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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;