mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2026-02-10 21:49:19 -06:00
feat/added AppFileMount model and integrate into existing structures
This commit is contained in:
13
prisma/migrations/20241223140802_migration/migration.sql
Normal file
13
prisma/migrations/20241223140802_migration/migration.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "AppFileMount" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"containerMountPath" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"appId" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "AppFileMount_appId_fkey" FOREIGN KEY ("appId") REFERENCES "App" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AppFileMount_appId_containerMountPath_key" ON "AppFileMount"("appId", "containerMountPath");
|
||||
@@ -147,9 +147,10 @@ model App {
|
||||
cpuReservation Int?
|
||||
cpuLimit Int?
|
||||
|
||||
appDomains AppDomain[]
|
||||
appVolumes AppVolume[]
|
||||
appPorts AppPort[]
|
||||
appDomains AppDomain[]
|
||||
appPorts AppPort[]
|
||||
appVolumes AppVolume[]
|
||||
appFileMounts AppFileMount[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -194,6 +195,19 @@ model AppVolume {
|
||||
@@unique([appId, containerMountPath])
|
||||
}
|
||||
|
||||
model AppFileMount {
|
||||
id String @id @default(uuid())
|
||||
containerMountPath String
|
||||
content String
|
||||
appId String
|
||||
app App @relation(fields: [appId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([appId, containerMountPath])
|
||||
}
|
||||
|
||||
model Parameter {
|
||||
name String @id
|
||||
value String
|
||||
|
||||
@@ -7,7 +7,7 @@ import GeneralAppSource from "./general/app-source";
|
||||
import EnvEdit from "./environment/env-edit";
|
||||
import { App } from "@prisma/client";
|
||||
import DomainsList from "./domains/domains";
|
||||
import StorageList from "./storage/storages";
|
||||
import StorageList from "./volumes/storages";
|
||||
import { AppExtendedModel } from "@/shared/model/app-extended.model";
|
||||
import { BuildJobModel } from "@/shared/model/build-job";
|
||||
import BuildsTab from "./overview/deployments";
|
||||
@@ -17,6 +17,7 @@ import InternalHostnames from "./domains/ports-and-internal-hostnames";
|
||||
import TerminalStreamed from "./overview/terminal-streamed";
|
||||
import { useEffect } from "react";
|
||||
import { useBreadcrumbs } from "@/frontend/states/zustand.states";
|
||||
import FileMount from "./volumes/file-mount";
|
||||
|
||||
export default function AppTabs({
|
||||
app,
|
||||
@@ -58,6 +59,7 @@ export default function AppTabs({
|
||||
</TabsContent>
|
||||
<TabsContent value="storage" className="space-y-4">
|
||||
<StorageList app={app} />
|
||||
<FileMount app={app} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getAuthUserSession, saveFormAction, simpleAction } from "@/server/utils
|
||||
import { z } from "zod";
|
||||
import { ServiceException } from "@/shared/model/service.exception.model";
|
||||
import pvcService from "@/server/services/pvc.service";
|
||||
import { fileMountEditZodModel } from "@/shared/model/file-mount-edit.model";
|
||||
|
||||
const actionAppVolumeEditZodModel = appVolumeEditZodModel.merge(z.object({
|
||||
appId: z.string(),
|
||||
@@ -31,10 +32,10 @@ export const saveVolume = async (prevState: any, inputData: z.infer<typeof actio
|
||||
});
|
||||
});
|
||||
|
||||
export const deleteVolume = async (volumeID: string) =>
|
||||
export const deleteVolume = async (volumeId: string) =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
await appService.deleteVolumeById(volumeID);
|
||||
await appService.deleteVolumeById(volumeId);
|
||||
return new SuccessActionResult(undefined, 'Successfully deleted volume');
|
||||
});
|
||||
|
||||
@@ -42,7 +43,7 @@ export const getPvcUsage = async (appId: string, projectId: string) =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
return pvcService.getPvcUsageFromApp(appId, projectId);
|
||||
}) as Promise<ServerActionResult<any, {pvcName: string, usage: number}[]>>;
|
||||
}) as Promise<ServerActionResult<any, { pvcName: string, usage: number }[]>>;
|
||||
|
||||
export const downloadPvcData = async (volumeId: string) =>
|
||||
simpleAction(async () => {
|
||||
@@ -50,3 +51,25 @@ export const downloadPvcData = async (volumeId: string) =>
|
||||
const fileNameOfDownloadedFile = await pvcService.downloadPvcData(volumeId);
|
||||
return new SuccessActionResult(fileNameOfDownloadedFile, 'Successfully zipped volume data'); // returns the download path on the server
|
||||
}) as Promise<ServerActionResult<any, string>>;
|
||||
|
||||
const actionAppFileMountEditZodModel = fileMountEditZodModel.merge(z.object({
|
||||
appId: z.string(),
|
||||
id: z.string().nullish()
|
||||
}));
|
||||
|
||||
export const saveFileMount = async (prevState: any, inputData: z.infer<typeof actionAppFileMountEditZodModel>) =>
|
||||
saveFormAction(inputData, actionAppFileMountEditZodModel, async (validatedData) => {
|
||||
await getAuthUserSession();
|
||||
const existingApp = await appService.getExtendedById(validatedData.appId);
|
||||
await appService.saveFileMount({
|
||||
...validatedData,
|
||||
id: validatedData.id ?? undefined,
|
||||
});
|
||||
});
|
||||
|
||||
export const deleteFileMount = async (fileMountId: string) =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
await appService.deleteFileMountById(fileMountId);
|
||||
return new SuccessActionResult(undefined, 'Successfully deleted volume');
|
||||
});
|
||||
124
src/app/project/app/[appId]/volumes/file-mount-edit-dialog.tsx
Normal file
124
src/app/project/app/[appId]/volumes/file-mount-edit-dialog.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
'use client'
|
||||
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useFormState } from 'react-dom'
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormUtils } from "@/frontend/utils/form.utilts";
|
||||
import { SubmitButton } from "@/components/custom/submit-button";
|
||||
import { AppFileMount } from "@prisma/client"
|
||||
import { ServerActionResult } from "@/shared/model/server-action-error-return.model"
|
||||
import { saveFileMount } from "./actions"
|
||||
import { toast } from "sonner"
|
||||
import { AppExtendedModel } from "@/shared/model/app-extended.model"
|
||||
import { FileMountEditModel, fileMountEditZodModel } from "@/shared/model/file-mount-edit.model"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
|
||||
const accessModes = [
|
||||
{ label: "ReadWriteOnce", value: "ReadWriteOnce" },
|
||||
{ label: "ReadWriteMany", value: "ReadWriteMany" },
|
||||
] as const
|
||||
|
||||
export default function FileMountEditDialog({ children, fileMount, app }: { children: React.ReactNode; fileMount?: AppFileMount; app: AppExtendedModel; }) {
|
||||
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
|
||||
|
||||
const form = useForm<FileMountEditModel>({
|
||||
resolver: zodResolver(fileMountEditZodModel),
|
||||
defaultValues: {
|
||||
...fileMount,
|
||||
}
|
||||
});
|
||||
|
||||
const [state, formAction] = useFormState((state: ServerActionResult<any, any>, payload: FileMountEditModel) =>
|
||||
saveFileMount(state, {
|
||||
...payload,
|
||||
appId: app.id,
|
||||
id: fileMount?.id
|
||||
}), FormUtils.getInitialFormState<typeof fileMountEditZodModel>());
|
||||
|
||||
useEffect(() => {
|
||||
if (state.status === 'success') {
|
||||
form.reset();
|
||||
toast.success('File Mount saved successfully', {
|
||||
description: "Click \"deploy\" to apply the changes to your app.",
|
||||
});
|
||||
setIsOpen(false);
|
||||
}
|
||||
FormUtils.mapValidationErrorsToForm<typeof fileMountEditZodModel>(state, form);
|
||||
}, [state]);
|
||||
|
||||
useEffect(() => {
|
||||
form.reset(fileMount);
|
||||
}, [fileMount]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div onClick={() => setIsOpen(true)}>
|
||||
{children}
|
||||
</div>
|
||||
<Dialog open={!!isOpen} onOpenChange={(isOpened) => setIsOpen(false)}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit File Mount</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configure your custom file mount. The content of the file mount will be available in the container at the specified mount path.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form action={(e) => form.handleSubmit((data) => {
|
||||
return formAction(data);
|
||||
})()}>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="containerMountPath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Mount Path Container</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="ex. /data/my-config.txt" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="content"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>File Content</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea rows={10} placeholder="Write your file content here..." {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<p className="text-red-500">{state.message}</p>
|
||||
<SubmitButton>Save</SubmitButton>
|
||||
</div>
|
||||
</form>
|
||||
</Form >
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
|
||||
|
||||
|
||||
}
|
||||
75
src/app/project/app/[appId]/volumes/file-mount.tsx
Normal file
75
src/app/project/app/[appId]/volumes/file-mount.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
'use client';
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { AppExtendedModel } from "@/shared/model/app-extended.model";
|
||||
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Download, EditIcon, TrashIcon } from "lucide-react";
|
||||
import DialogEditDialog from "./storage-edit-overlay";
|
||||
import { Toast } from "@/frontend/utils/toast.utils";
|
||||
import { deleteFileMount, deleteVolume, downloadPvcData, getPvcUsage } from "./actions";
|
||||
import { useConfirmDialog } from "@/frontend/states/zustand.states";
|
||||
import { AppVolume } from "@prisma/client";
|
||||
import React from "react";
|
||||
import { KubeObjectNameUtils } from "@/server/utils/kube-object-name.utils";
|
||||
import FileMountEditDialog from "./file-mount-edit-dialog";
|
||||
|
||||
type AppVolumeWithCapacity = (AppVolume & { capacity?: string });
|
||||
|
||||
export default function FileMount({ app }: {
|
||||
app: AppExtendedModel
|
||||
}) {
|
||||
|
||||
const { openConfirmDialog: openDialog } = useConfirmDialog();
|
||||
|
||||
const asyncDeleteFileMount = async (volumeId: string) => {
|
||||
const confirm = await openDialog({
|
||||
title: "Delete File Mount",
|
||||
description: "The file mount will be removed and the Data will be lost. The changes will take effect, after you deploy the app. Are you sure you want to remove this file mount?",
|
||||
okButton: "Delete File Mount",
|
||||
});
|
||||
if (confirm) {
|
||||
await Toast.fromAction(() => deleteFileMount(volumeId));
|
||||
}
|
||||
};
|
||||
|
||||
return <>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>File Mount</CardTitle>
|
||||
<CardDescription>Create files wich are mounted into the container.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableCaption>{app.appFileMounts.length} File Mounts</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Mount Path</TableHead>
|
||||
<TableHead className="w-[100px]">Action</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{app.appFileMounts.map(fileMount => (
|
||||
<TableRow key={fileMount.containerMountPath}>
|
||||
<TableCell className="font-medium">{fileMount.containerMountPath}</TableCell>
|
||||
<TableCell className="font-medium flex gap-2">
|
||||
<FileMountEditDialog app={app} fileMount={fileMount}>
|
||||
<Button variant="ghost"><EditIcon /></Button>
|
||||
</FileMountEditDialog>
|
||||
<Button variant="ghost" onClick={() => asyncDeleteFileMount(fileMount.id)}>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<FileMountEditDialog app={app}>
|
||||
<Button>Add File Mount</Button>
|
||||
</FileMountEditDialog>
|
||||
</CardFooter>
|
||||
</Card >
|
||||
</>;
|
||||
}
|
||||
@@ -74,8 +74,8 @@ export default function StorageList({ app }: {
|
||||
return <>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Storage</CardTitle>
|
||||
<CardDescription>Add one or more volumes to your application.</CardDescription>
|
||||
<CardTitle>Volumes</CardTitle>
|
||||
<CardDescription>Add one or more volumes to to configure persistent storage within your container.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
@@ -1,14 +1,13 @@
|
||||
import { revalidateTag, unstable_cache } from "next/cache";
|
||||
import dataAccess from "../adapter/db.client";
|
||||
import { Tags } from "../utils/cache-tag-generator.utils";
|
||||
import { App, AppDomain, AppPort, AppVolume, Prisma } from "@prisma/client";
|
||||
import { App, AppDomain, AppFileMount, AppPort, AppVolume, Prisma } from "@prisma/client";
|
||||
import { DefaultArgs } from "@prisma/client/runtime/library";
|
||||
import { AppExtendedModel } from "@/shared/model/app-extended.model";
|
||||
import { ServiceException } from "@/shared/model/service.exception.model";
|
||||
import { KubeObjectNameUtils } from "../utils/kube-object-name.utils";
|
||||
import deploymentService from "./deployment.service";
|
||||
import buildService from "./build.service";
|
||||
import namespaceService from "./namespace.service";
|
||||
import ingressService from "./ingress.service";
|
||||
import pvcService from "./pvc.service";
|
||||
import svcService from "./svc.service";
|
||||
@@ -86,7 +85,8 @@ class AppService {
|
||||
project: true,
|
||||
appDomains: true,
|
||||
appVolumes: true,
|
||||
appPorts: true
|
||||
appPorts: true,
|
||||
appFileMounts: true,
|
||||
};
|
||||
if (cached) {
|
||||
return await unstable_cache(async (id: string) => await dataAccess.client.app.findFirstOrThrow({
|
||||
@@ -277,6 +277,64 @@ class AppService {
|
||||
}
|
||||
}
|
||||
|
||||
async saveFileMount(fileMountToBeSaved: Prisma.AppFileMountUncheckedCreateInput | Prisma.AppFileMountUncheckedUpdateInput) {
|
||||
let savedItem: AppFileMount;
|
||||
const existingApp = await this.getExtendedById(fileMountToBeSaved.appId as string);
|
||||
const existingAppWithSameVolumeMountPath = await dataAccess.client.appFileMount.findMany({
|
||||
where: {
|
||||
appId: fileMountToBeSaved.appId as string,
|
||||
}
|
||||
});
|
||||
|
||||
if (existingAppWithSameVolumeMountPath.filter(x => x.id !== fileMountToBeSaved.id)
|
||||
.some(x => x.containerMountPath === fileMountToBeSaved.containerMountPath)) {
|
||||
throw new ServiceException("Mount Path is already configured within the same app.");
|
||||
}
|
||||
|
||||
try {
|
||||
if (fileMountToBeSaved.id) {
|
||||
savedItem = await dataAccess.client.appFileMount.update({
|
||||
where: {
|
||||
id: fileMountToBeSaved.id as string
|
||||
},
|
||||
data: fileMountToBeSaved
|
||||
});
|
||||
} else {
|
||||
savedItem = await dataAccess.client.appFileMount.create({
|
||||
data: fileMountToBeSaved as Prisma.AppFileMountUncheckedCreateInput
|
||||
});
|
||||
}
|
||||
|
||||
} finally {
|
||||
revalidateTag(Tags.apps(existingApp.projectId as string));
|
||||
revalidateTag(Tags.app(existingApp.id as string));
|
||||
}
|
||||
return savedItem;
|
||||
}
|
||||
|
||||
async deleteFileMountById(id: string) {
|
||||
const existingVolume = await dataAccess.client.appFileMount.findFirst({
|
||||
where: {
|
||||
id
|
||||
}, include: {
|
||||
app: true
|
||||
}
|
||||
});
|
||||
if (!existingVolume) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await dataAccess.client.appFileMount.delete({
|
||||
where: {
|
||||
id
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
revalidateTag(Tags.app(existingVolume.appId));
|
||||
revalidateTag(Tags.apps(existingVolume.app.projectId));
|
||||
}
|
||||
}
|
||||
|
||||
async savePort(portToBeSaved: Prisma.AppPortUncheckedCreateInput | Prisma.AppPortUncheckedUpdateInput) {
|
||||
let savedItem: AppPort;
|
||||
const existingApp = await this.getExtendedById(portToBeSaved.appId as string);
|
||||
|
||||
91
src/server/services/config-map.service.ts
Normal file
91
src/server/services/config-map.service.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { AppExtendedModel } from "@/shared/model/app-extended.model";
|
||||
import k3s from "../adapter/kubernetes-api.adapter";
|
||||
import { KubeObjectNameUtils } from "../utils/kube-object-name.utils";
|
||||
import { Constants } from "../../shared/utils/constants";
|
||||
import { PathUtils } from "../utils/path.utils";
|
||||
import * as k8s from '@kubernetes/client-node';
|
||||
import { dlog } from "./deployment-logs.service";
|
||||
|
||||
class ConfigMapService {
|
||||
|
||||
private async getConfigMapsForApp(projectId: string, appId: string) {
|
||||
const configMaps = await k3s.core.listNamespacedConfigMap(projectId);
|
||||
|
||||
return configMaps.body.items.filter(cm => {
|
||||
return cm.metadata?.annotations?.[Constants.QS_ANNOTATION_APP_ID] === appId;
|
||||
});
|
||||
}
|
||||
|
||||
async createOrUpdateConfigMapForApp(app: AppExtendedModel) {
|
||||
|
||||
const existingConfigMaps = await this.getConfigMapsForApp(app.projectId, app.id);
|
||||
|
||||
if (app.appFileMounts.length === 0) {
|
||||
return { fileVolumeMounts: [], fileVolumes: [] };
|
||||
}
|
||||
|
||||
const fileVolumeMounts: k8s.V1VolumeMount[] = [];
|
||||
const fileVolumes: k8s.V1Volume[] = [];
|
||||
|
||||
for (const fileMount of app.appFileMounts) {
|
||||
const currentConfigMapName = KubeObjectNameUtils.getConfigMapName(fileMount.id);
|
||||
|
||||
let { folderPath, filePath } = PathUtils.splitPath(fileMount.containerMountPath);
|
||||
if (!folderPath) {
|
||||
folderPath = '/';
|
||||
}
|
||||
|
||||
const configMapManifest = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'ConfigMap',
|
||||
metadata: {
|
||||
name: currentConfigMapName,
|
||||
namespace: app.projectId,
|
||||
annotations: {
|
||||
[Constants.QS_ANNOTATION_APP_ID]: app.id,
|
||||
[Constants.QS_ANNOTATION_PROJECT_ID]: app.projectId,
|
||||
'qs-app-file-mount-id': fileMount.id,
|
||||
}
|
||||
},
|
||||
data: {
|
||||
[filePath]: fileMount.content
|
||||
},
|
||||
};
|
||||
|
||||
if (existingConfigMaps.some(cm => cm.metadata!.name === currentConfigMapName)) {
|
||||
await k3s.core.replaceNamespacedConfigMap(currentConfigMapName, app.projectId, configMapManifest);
|
||||
} else {
|
||||
await k3s.core.createNamespacedConfigMap(app.projectId, configMapManifest);
|
||||
}
|
||||
|
||||
fileVolumeMounts.push({
|
||||
name: currentConfigMapName,
|
||||
mountPath: fileMount.containerMountPath,
|
||||
subPath: filePath,
|
||||
readOnly: true
|
||||
});
|
||||
|
||||
fileVolumes.push({
|
||||
name: currentConfigMapName,
|
||||
configMap: {
|
||||
name: currentConfigMapName,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { fileVolumeMounts, fileVolumes };
|
||||
}
|
||||
|
||||
|
||||
async deleteUnusedConfigMaps(app: AppExtendedModel) {
|
||||
const existingConfigMaps = await this.getConfigMapsForApp(app.projectId, app.id);
|
||||
for (const cm of existingConfigMaps) {
|
||||
if (!app.appFileMounts.some(fm => KubeObjectNameUtils.getConfigMapName(fm.id) === cm.metadata?.name)) {
|
||||
await k3s.core.deleteNamespacedConfigMap(cm.metadata!.name!, app.projectId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const configMapService = new ConfigMapService();
|
||||
export default configMapService;
|
||||
@@ -14,6 +14,7 @@ import svcService from "./svc.service";
|
||||
import { dlog } from "./deployment-logs.service";
|
||||
import registryService from "./registry.service";
|
||||
import { EnvVarUtils } from "../utils/env-var.utils";
|
||||
import configMapService from "./config-map.service";
|
||||
|
||||
class DeploymentService {
|
||||
|
||||
@@ -58,6 +59,14 @@ class DeploymentService {
|
||||
dlog(deploymentId, `Configured ${volumes.length} Storage Volumes.`);
|
||||
}
|
||||
|
||||
const { fileVolumeMounts, fileVolumes } = await configMapService.createOrUpdateConfigMapForApp(app);
|
||||
if (fileVolumes && fileVolumes.length > 0) {
|
||||
dlog(deploymentId, `Configured ${fileVolumes.length} File Mounts.`);
|
||||
}
|
||||
|
||||
const allVolumes = [...volumes, ...fileVolumes];
|
||||
const allVolumeMounts = [...volumeMounts, ...fileVolumeMounts];
|
||||
|
||||
const envVars = EnvVarUtils.parseEnvVariables(app);
|
||||
dlog(deploymentId, `Configured ${envVars.length} Env Variables.`);
|
||||
|
||||
@@ -93,10 +102,10 @@ class DeploymentService {
|
||||
image: !!buildJobName ? registryService.createContainerRegistryUrlForAppId(app.id) : app.containerImageSource as string,
|
||||
imagePullPolicy: 'Always',
|
||||
...(envVars.length > 0 ? { env: envVars } : {}),
|
||||
...(volumeMounts.length > 0 ? { volumeMounts: volumeMounts } : {}),
|
||||
...(allVolumeMounts.length > 0 ? { volumeMounts: allVolumeMounts } : {}),
|
||||
}
|
||||
],
|
||||
...(volumes.length > 0 ? { volumes: volumes } : {}),
|
||||
...(allVolumes.length > 0 ? { volumes: allVolumes } : {}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,6 +163,7 @@ class DeploymentService {
|
||||
dlog(deploymentId, `Creating deployment...`);
|
||||
const res = await k3s.apps.createNamespacedDeployment(app.projectId, body);
|
||||
}
|
||||
await configMapService.deleteUnusedConfigMaps(app);
|
||||
await pvcService.deleteUnusedPvcOfApp(app);
|
||||
await svcService.createOrUpdateService(deploymentId, app);
|
||||
dlog(deploymentId, `Updating ingress...`);
|
||||
|
||||
@@ -56,4 +56,8 @@ export class KubeObjectNameUtils {
|
||||
static getIngressName(domainId: string): `ingress-${string}` {
|
||||
return `ingress-${domainId}`;
|
||||
}
|
||||
|
||||
static getConfigMapName(id: string): `cm-${string}` {
|
||||
return `cm-${id}`;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { AppDomainModel, AppModel, AppPortModel, AppVolumeModel, ProjectModel, RelatedAppModel } from "./generated-zod";
|
||||
import { AppDomainModel, AppFileMountModel, AppModel, AppPortModel, AppVolumeModel, ProjectModel, RelatedAppModel } from "./generated-zod";
|
||||
|
||||
|
||||
export const AppExtendedZodModel= z.lazy(() => AppModel.extend({
|
||||
@@ -7,6 +7,7 @@ export const AppExtendedZodModel= z.lazy(() => AppModel.extend({
|
||||
appDomains: AppDomainModel.array(),
|
||||
appVolumes: AppVolumeModel.array(),
|
||||
appPorts: AppPortModel.array(),
|
||||
appFileMounts: AppFileMountModel.array(),
|
||||
}))
|
||||
|
||||
export type AppExtendedModel = z.infer<typeof AppExtendedZodModel>;
|
||||
8
src/shared/model/file-mount-edit.model.ts
Normal file
8
src/shared/model/file-mount-edit.model.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const fileMountEditZodModel = z.object({
|
||||
containerMountPath: z.string().trim().min(1),
|
||||
content: z.string().min(1),
|
||||
})
|
||||
|
||||
export type FileMountEditModel = z.infer<typeof fileMountEditZodModel>;
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as z from "zod"
|
||||
|
||||
import { CompleteProject, RelatedProjectModel, CompleteAppDomain, RelatedAppDomainModel, CompleteAppVolume, RelatedAppVolumeModel, CompleteAppPort, RelatedAppPortModel } from "./index"
|
||||
import { CompleteProject, RelatedProjectModel, CompleteAppDomain, RelatedAppDomainModel, CompleteAppPort, RelatedAppPortModel, CompleteAppVolume, RelatedAppVolumeModel, CompleteAppFileMount, RelatedAppFileMountModel } from "./index"
|
||||
|
||||
export const AppModel = z.object({
|
||||
id: z.string(),
|
||||
@@ -27,8 +27,9 @@ export const AppModel = z.object({
|
||||
export interface CompleteApp extends z.infer<typeof AppModel> {
|
||||
project: CompleteProject
|
||||
appDomains: CompleteAppDomain[]
|
||||
appVolumes: CompleteAppVolume[]
|
||||
appPorts: CompleteAppPort[]
|
||||
appVolumes: CompleteAppVolume[]
|
||||
appFileMounts: CompleteAppFileMount[]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,6 +40,7 @@ export interface CompleteApp extends z.infer<typeof AppModel> {
|
||||
export const RelatedAppModel: z.ZodSchema<CompleteApp> = z.lazy(() => AppModel.extend({
|
||||
project: RelatedProjectModel,
|
||||
appDomains: RelatedAppDomainModel.array(),
|
||||
appVolumes: RelatedAppVolumeModel.array(),
|
||||
appPorts: RelatedAppPortModel.array(),
|
||||
appVolumes: RelatedAppVolumeModel.array(),
|
||||
appFileMounts: RelatedAppFileMountModel.array(),
|
||||
}))
|
||||
|
||||
25
src/shared/model/generated-zod/appfilemount.ts
Normal file
25
src/shared/model/generated-zod/appfilemount.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as z from "zod"
|
||||
|
||||
import { CompleteApp, RelatedAppModel } from "./index"
|
||||
|
||||
export const AppFileMountModel = z.object({
|
||||
id: z.string(),
|
||||
containerMountPath: z.string(),
|
||||
content: z.string(),
|
||||
appId: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
})
|
||||
|
||||
export interface CompleteAppFileMount extends z.infer<typeof AppFileMountModel> {
|
||||
app: CompleteApp
|
||||
}
|
||||
|
||||
/**
|
||||
* RelatedAppFileMountModel contains all relations on your model in addition to the scalars
|
||||
*
|
||||
* NOTE: Lazy required in case of potential circular dependencies within schema
|
||||
*/
|
||||
export const RelatedAppFileMountModel: z.ZodSchema<CompleteAppFileMount> = z.lazy(() => AppFileMountModel.extend({
|
||||
app: RelatedAppModel,
|
||||
}))
|
||||
@@ -8,4 +8,5 @@ export * from "./app"
|
||||
export * from "./appport"
|
||||
export * from "./appdomain"
|
||||
export * from "./appvolume"
|
||||
export * from "./appfilemount"
|
||||
export * from "./parameter"
|
||||
|
||||
@@ -45,7 +45,7 @@ export const postgreAppTemplate: AppTemplateModel = {
|
||||
appDomains: [],
|
||||
appVolumes: [{
|
||||
size: 500,
|
||||
containerMountPath: '/var/lib/postgresql/data',
|
||||
containerMountPath: '/var/lib/postgresql',
|
||||
accessMode: 'ReadWriteOnce'
|
||||
}],
|
||||
appPorts: [{
|
||||
|
||||
Reference in New Issue
Block a user