mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2026-01-01 17:20:14 -06:00
improvements pvc
This commit is contained in:
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||
113
src/server/services/ingress.service.ts
Normal file
113
src/server/services/ingress.service.ts
Normal 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;
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user