improvements pvc

This commit is contained in:
biersoeckli
2024-11-07 12:56:58 +00:00
parent cdc282119b
commit aeaf3d8261
4 changed files with 151 additions and 112 deletions

View File

@@ -5,6 +5,7 @@ import { SuccessActionResult } from "@/model/server-action-error-return.model";
import appService from "@/server/services/app.service";
import { getAuthUserSession, saveFormAction, simpleAction } from "@/server/utils/action-wrapper.utils";
import { z } from "zod";
import { ServiceException } from "@/model/service.exception.model";
const actionAppVolumeEditZodModel = appVolumeEditZodModel.merge(z.object({
appId: z.string(),
@@ -14,15 +15,20 @@ const actionAppVolumeEditZodModel = appVolumeEditZodModel.merge(z.object({
export const saveVolume = async (prevState: any, inputData: z.infer<typeof actionAppVolumeEditZodModel>) =>
saveFormAction(inputData, actionAppVolumeEditZodModel, async (validatedData) => {
await getAuthUserSession();
const existingVolume = validatedData.id ? await appService.getVolumeById(validatedData.id) : undefined;
if (existingVolume && existingVolume.size > validatedData.size) {
throw new ServiceException('Volume size cannot be decreased');
}
await appService.saveVolume({
...validatedData,
id: validatedData.id ?? undefined
id: validatedData.id ?? undefined,
accessMode: existingVolume?.accessMode ?? validatedData.accessMode as string
});
});
export const deleteVolume = async (volumeID: string) =>
simpleAction(async () => {
await getAuthUserSession();
await appService.deleteVolumeById(volumeID);
return new SuccessActionResult(undefined, 'Successfully deleted volume');
});
export const deleteVolume = async (volumeID: string) =>
simpleAction(async () => {
await getAuthUserSession();
await appService.deleteVolumeById(volumeID);
return new SuccessActionResult(undefined, 'Successfully deleted volume');
});

View File

@@ -9,6 +9,7 @@ import { ServiceException } from "@/model/service.exception.model";
import { PodsInfoModel } from "@/model/pods-info.model";
import { StringUtils } from "../utils/string.utils";
import pvcService from "./pvc.service";
import ingressService from "./ingress.service";
class DeploymentService {
@@ -87,7 +88,6 @@ class DeploymentService {
} else {
await k3s.core.createNamespacedService(app.projectId, body);
}
await this.createOrUpdateIngress(app);
}
@@ -101,7 +101,7 @@ class DeploymentService {
await this.validateDeployment(app);
await this.createNamespaceIfNotExists(app.projectId);
if (await pvcService.doesAppConfigurationIncreaseAnyPvcSize(app)) {
await this.setReplicasForDeployment(app.projectId, app.id, 0); // update of PVCs is only possible if deployment is scaled down
// await this.setReplicasForDeployment(app.projectId, app.id, 0); // update of PVCs is only possible if deployment is scaled down
}
const { volumes, volumeMounts } = await pvcService.createOrUpdatePvc(app);
@@ -174,6 +174,7 @@ class DeploymentService {
}
await pvcService.deleteUnusedPvcOfApp(app);
await this.createOrUpdateService(app);
await ingressService.createOrUpdateIngress(app);
}
async setReplicasForDeployment(projectId: string, appId: string, replicas: number) {
@@ -221,108 +222,6 @@ class DeploymentService {
}
async getAllIngressForApp(projectId: string, appId: string) {
const res = await k3s.network.listNamespacedIngress(projectId);
return res.body.items.filter((item) => item.metadata?.name?.startsWith(`ingress-${appId}`));
}
async getIngress(projectId: string, appId: string, domainId: string) {
const res = await k3s.network.listNamespacedIngress(projectId);
return res.body.items.find((item) => item.metadata?.name === `ingress-${appId}-${domainId}`);
}
async deleteObsoleteIngresses(app: AppExtendedModel) {
const currentDomains = new Set(app.appDomains.map(domainObj => domainObj.hostname));
const existingIngresses = await this.getAllIngressForApp(app.projectId, app.id);
if (currentDomains.size === 0) {
for (const ingress of existingIngresses) {
try {
await k3s.network.deleteNamespacedIngress(ingress.metadata!.name!, app.projectId);
console.log(`Alle Ingress-Konfigurationen für die App ${app.id} erfolgreich gelöscht.`);
} catch (error) {
console.error(`Fehler beim Löschen des Ingress ${ingress.metadata!.name}:`, error);
}
}
} else {
for (const ingress of existingIngresses) {
const ingressDomain = ingress.spec?.rules?.[0]?.host;
if (ingressDomain && !currentDomains.has(ingressDomain)) {
try {
await k3s.network.deleteNamespacedIngress(ingress.metadata!.name!, app.projectId);
console.log(`Ingress ${ingress.metadata!.name} für Domain ${ingressDomain} erfolgreich gelöscht.`);
} catch (error) {
console.error(`Fehler beim Löschen des Ingress ${ingress.metadata!.name} für Domain ${ingressDomain}:`, error);
}
}
}
}
}
async createOrUpdateIngress(app: AppExtendedModel) {
for (const domainObj of app.appDomains) {
const domain = domainObj.hostname;
const ingressName = `ingress-${app.id}-${domainObj.id}`;
const existingIngress = await this.getIngress(app.projectId, app.id, domainObj.id);
const ingressDefinition: V1Ingress = {
apiVersion: 'networking.k8s.io/v1',
kind: 'Ingress',
metadata: {
name: ingressName,
namespace: app.projectId,
annotations: {
...(domainObj.useSsl === true && { 'cert-manager.io/cluster-issuer': 'letsencrypt-production' }),
},
},
spec: {
ingressClassName: 'traefik',
rules: [
{
host: domain,
http: {
paths: [
{
path: '/',
pathType: 'Prefix',
backend: {
service: {
name: StringUtils.toServiceName(app.id),
port: {
number: app.defaultPort,
},
},
},
},
],
},
},
],
...(domainObj.useSsl === true && {
tls: [
{
hosts: [domain],
secretName: `secret-tls-${app.id}-${domainObj.id}`,
},
],
}),
},
};
if (existingIngress) {
await k3s.network.replaceNamespacedIngress(ingressName, app.projectId, ingressDefinition);
console.log(`Ingress ${ingressName} für Domain ${domain} erfolgreich aktualisiert.`);
} else {
await k3s.network.createNamespacedIngress(app.projectId, ingressDefinition);
console.log(`Ingress ${ingressName} für Domain ${domain} erfolgreich erstellt.`);
}
}
await this.deleteObsoleteIngresses(app);
}
/**
* 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

@@ -0,0 +1,113 @@
import { AppExtendedModel } from "@/model/app-extended.model";
import k3s from "../adapter/kubernetes-api.adapter";
import { V1Ingress, V1PersistentVolumeClaim } from "@kubernetes/client-node";
import { StringUtils } from "../utils/string.utils";
class IngressService {
async getAllIngressForApp(projectId: string, appId: string) {
const res = await k3s.network.listNamespacedIngress(projectId);
return res.body.items.filter((item) => item.metadata?.name?.startsWith(`ingress-${appId}`));
}
async getIngress(projectId: string, appId: string, domainId: string) {
const res = await k3s.network.listNamespacedIngress(projectId);
return res.body.items.find((item) => item.metadata?.name === `ingress-${appId}-${domainId}`);
}
async deleteObsoleteIngresses(app: AppExtendedModel) {
const currentDomains = new Set(app.appDomains.map(domainObj => domainObj.hostname));
const existingIngresses = await this.getAllIngressForApp(app.projectId, app.id);
if (currentDomains.size === 0) {
for (const ingress of existingIngresses) {
try {
await k3s.network.deleteNamespacedIngress(ingress.metadata!.name!, app.projectId);
console.log(`Alle Ingress-Konfigurationen für die App ${app.id} erfolgreich gelöscht.`);
} catch (error) {
console.error(`Fehler beim Löschen des Ingress ${ingress.metadata!.name}:`, error);
}
}
} else {
for (const ingress of existingIngresses) {
const ingressDomain = ingress.spec?.rules?.[0]?.host;
if (ingressDomain && !currentDomains.has(ingressDomain)) {
try {
await k3s.network.deleteNamespacedIngress(ingress.metadata!.name!, app.projectId);
console.log(`Ingress ${ingress.metadata!.name} für Domain ${ingressDomain} erfolgreich gelöscht.`);
} catch (error) {
console.error(`Fehler beim Löschen des Ingress ${ingress.metadata!.name} für Domain ${ingressDomain}:`, error);
}
}
}
}
}
async createOrUpdateIngress(app: AppExtendedModel) {
for (const domainObj of app.appDomains) {
const domain = domainObj.hostname;
const ingressName = `ingress-${app.id}-${domainObj.id}`;
const existingIngress = await this.getIngress(app.projectId, app.id, domainObj.id);
const ingressDefinition: V1Ingress = {
apiVersion: 'networking.k8s.io/v1',
kind: 'Ingress',
metadata: {
name: ingressName,
namespace: app.projectId,
annotations: {
...(domainObj.useSsl === true && { 'cert-manager.io/cluster-issuer': 'letsencrypt-production' }),
},
},
spec: {
ingressClassName: 'traefik',
rules: [
{
host: domain,
http: {
paths: [
{
path: '/',
pathType: 'Prefix',
backend: {
service: {
name: StringUtils.toServiceName(app.id),
port: {
number: app.defaultPort,
},
},
},
},
],
},
},
],
...(domainObj.useSsl === true && {
tls: [
{
hosts: [domain],
secretName: `secret-tls-${app.id}-${domainObj.id}`,
},
],
}),
},
};
if (existingIngress) {
await k3s.network.replaceNamespacedIngress(ingressName, app.projectId, ingressDefinition);
console.log(`Ingress ${ingressName} für Domain ${domain} erfolgreich aktualisiert.`);
} else {
await k3s.network.createNamespacedIngress(app.projectId, ingressDefinition);
console.log(`Ingress ${ingressName} für Domain ${domain} erfolgreich erstellt.`);
}
}
await this.deleteObsoleteIngresses(app);
}
}
const ingressService = new IngressService();
export default ingressService;

View File

@@ -1,6 +1,8 @@
import { AppExtendedModel } from "@/model/app-extended.model";
import k3s from "../adapter/kubernetes-api.adapter";
import { V1PersistentVolumeClaim } from "@kubernetes/client-node";
import { V1PersistentVolumeClaim } from "@kubernetes/client-node";
import { ServiceException } from "@/model/service.exception.model";
import { AppVolume } from "@prisma/client";
class PvcService {
@@ -76,6 +78,11 @@ class PvcService {
await k3s.core.replaceNamespacedPersistentVolumeClaim(pvcName, app.projectId, existingPvc);
console.log(`Updated PVC ${pvcName} for app ${app.id}`);
// wait until persisten volume ist resized
console.log(`Waiting for PV ${existingPvc.spec!.volumeName} to be resized`);
await this.waitUntilPvResized(existingPvc.spec!.volumeName!, appVolume.size);
console.log(`PV ${existingPvc.spec!.volumeName} resized to ${appVolume.size}Gi`);
} else {
await k3s.core.createNamespacedPersistentVolumeClaim(app.projectId, pvcDefinition);
console.log(`Created PVC ${pvcName} for app ${app.id}`);
@@ -100,6 +107,20 @@ class PvcService {
return { volumes, volumeMounts };
}
private async waitUntilPvResized(persistentVolumeName: string, size: number) {
let iterationCount = 0;
let pv = await k3s.core.readPersistentVolume(persistentVolumeName);
while (pv.body.spec!.capacity!.storage !== `${size}Gi`) {
if (iterationCount > 30) {
console.error(`Timeout: PV ${persistentVolumeName} not resized to ${size}Gi`);
throw new ServiceException(`Timeout: Volume could not be resized to ${size}Gi`);
}
await new Promise(resolve => setTimeout(resolve, 3000)); // wait 5 Seconds, so that the PV is resized
pv = await k3s.core.readPersistentVolume(persistentVolumeName);
iterationCount++;
}
}
}
const pvcService = new PvcService();