implemented pvc deployment

This commit is contained in:
stefan.meyer
2024-11-06 21:37:11 +00:00
parent bff7e8bb11
commit bb14a2912b
7 changed files with 174 additions and 45 deletions

View File

@@ -0,0 +1,18 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_AppVolume" (
"id" TEXT NOT NULL PRIMARY KEY,
"containerMountPath" TEXT NOT NULL,
"size" INTEGER NOT NULL,
"accessMode" TEXT NOT NULL DEFAULT 'ReadWriteOnce',
"appId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "AppVolume_appId_fkey" FOREIGN KEY ("appId") REFERENCES "App" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_AppVolume" ("appId", "containerMountPath", "createdAt", "id", "size", "updatedAt") SELECT "appId", "containerMountPath", "createdAt", "id", "size", "updatedAt" FROM "AppVolume";
DROP TABLE "AppVolume";
ALTER TABLE "new_AppVolume" RENAME TO "AppVolume";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -170,6 +170,7 @@ model AppVolume {
id String @id @default(uuid())
containerMountPath String
size Int
accessMode String @default("rwo")
appId String
app App @relation(fields: [appId], references: [id])

View File

@@ -4,12 +4,27 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } f
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import {
Command,
CommandGroup,
CommandItem,
CommandList,
} from "@/components/ui/command"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { Input } from "@/components/ui/input"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Check, ChevronsUpDown } from "lucide-react"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { useFormState } from 'react-dom'
@@ -22,7 +37,10 @@ import { ServerActionResult } from "@/model/server-action-error-return.model"
import { saveVolume } from "./actions"
import { toast } from "sonner"
const accessModes = [
{ label: "ReadWriteOnce", value: "ReadWriteOnce" },
{ label: "ReadWriteMany", value: "ReadWriteMany" },
] as const
export default function DialogEditDialog({ children, volume, appId }: { children: React.ReactNode; volume?: AppVolume; appId: string; }) {
@@ -33,6 +51,7 @@ export default function DialogEditDialog({ children, volume, appId }: { children
resolver: zodResolver(appVolumeEditZodModel),
defaultValues: {
...volume,
accessMode: "ReadWriteOnce"
}
});
@@ -98,6 +117,68 @@ export default function DialogEditDialog({ children, volume, appId }: { children
</FormItem>
)}
/>
<FormField
control={form.control}
name="accessMode"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Access Mode</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-[200px] justify-between",
!field.value && "text-muted-foreground"
)}
>
{field.value
? accessModes.find(
(accessMode) => accessMode.value === field.value
)?.label
: "Select accessMode"}
<ChevronsUpDown className="opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandList>
<CommandGroup>
{accessModes.map((accessMode) => (
<CommandItem
value={accessMode.label}
key={accessMode.value}
onSelect={() => {
form.setValue("accessMode", accessMode.value)
}}
>
{accessMode.label}
<Check
className={cn(
"ml-auto",
accessMode.value === field.value
? "opacity-100"
: "opacity-0"
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<FormDescription>
This is the access mode that will be used for the volume.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<p className="text-red-500">{state.message}</p>
<SubmitButton>Save</SubmitButton>
</div>

View File

@@ -43,6 +43,7 @@ export default function StorageList({ app }: {
<TableRow>
<TableHead>Mount Path</TableHead>
<TableHead>Size in GB</TableHead>
<TableHead>Access Mode</TableHead>
<TableHead className="w-[100px]">Action</TableHead>
</TableRow>
</TableHeader>
@@ -51,6 +52,7 @@ export default function StorageList({ app }: {
<TableRow key={volume.containerMountPath}>
<TableCell className="font-medium">{volume.containerMountPath}</TableCell>
<TableCell className="font-medium">{volume.size}</TableCell>
<TableCell className="font-medium">{volume.accessMode}</TableCell>
<TableCell className="font-medium flex gap-2">
<DialogEditDialog appId={app.id} volume={volume}>
<Button variant="ghost"><EditIcon /></Button>

View File

@@ -1,11 +1,12 @@
import * as z from "zod"
import * as imports from "../../../prisma/null"
import { CompleteApp, RelatedAppModel } from "./index"
export const AppVolumeModel = z.object({
id: z.string(),
containerMountPath: z.string(),
size: z.number().int(),
accessMode: z.string(),
appId: z.string(),
createdAt: z.date(),
updatedAt: z.date(),

View File

@@ -1,9 +1,11 @@
import { stringToNumber } from "@/lib/zod.utils";
import { access } from "fs";
import { z } from "zod";
export const appVolumeEditZodModel = z.object({
containerMountPath: z.string().trim().min(1),
size: stringToNumber,
accessMode: z.string().min(1),
})
export type AppVolumeEditModel = z.infer<typeof appVolumeEditZodModel>;

View File

@@ -89,7 +89,7 @@ class DeploymentService {
async createDeployment(app: AppExtendedModel, buildJobName?: string) {
await this.createNamespaceIfNotExists(app.projectId);
//await this.createPersistentVolumeClaim(app);
await this.createOrUpdatePvc(app);
const envVars = app.envVars
? app.envVars.split(',').map(env => {
@@ -98,6 +98,22 @@ class DeploymentService {
})
: [];
const volumes = app.appVolumes
.filter(pvcObj => pvcObj.appId === app.id)
.map(pvcObj => ({
name: `pvc-${app.id}-${pvcObj.id}`,
persistentVolumeClaim: {
claimName: `pvc-${app.id}-${pvcObj.id}`,
},
}));
const volumeMounts = app.appVolumes
.filter(pvcObj => pvcObj.appId === app.id)
.map(pvcObj => ({
name: `pvc-${app.id}-${pvcObj.id}`,
mountPath: pvcObj.containerMountPath,
}));
const existingDeployment = await this.getDeployment(app.projectId, app.id);
const body: V1Deployment = {
metadata: {
@@ -129,27 +145,10 @@ class DeploymentService {
image: !!buildJobName ? buildService.createContainerRegistryUrlForAppId(app.id) : app.containerImageSource as string,
imagePullPolicy: 'Always',
...(envVars.length > 0 ? { env: envVars } : {}),
/*volumeMounts: [
{
name: 'pvc-test-stefan',
mountPath: '/data',
},
],*/
/*ports: [
{
containerPort: app.port
}
]*/
...(volumeMounts.length > 0 ? { volumeMounts: volumeMounts } : {}),
}
],
/*volumes: [
{
name: 'pvc-test-stefan',
persistentVolumeClaim: {
claimName: 'pvc-test-stefan',
},
},
]*/
...(volumes.length > 0 ? { volumes: volumes } : {}),
}
}
}
@@ -215,6 +214,11 @@ class DeploymentService {
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);
@@ -244,12 +248,6 @@ class DeploymentService {
}
}
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 createOrUpdateIngress(app: AppExtendedModel) {
for (const domainObj of app.appDomains) {
const domain = domainObj.hostname;
@@ -313,26 +311,52 @@ class DeploymentService {
await this.deleteObsoleteIngresses(app);
}
async createPersistentVolumeClaim(app: AppExtendedModel) {
const pvcDefinition: V1PersistentVolumeClaim = {
apiVersion: 'v1',
kind: 'PersistentVolumeClaim',
metadata: {
name: 'pvc-test-stefan',
namespace: app.projectId,
},
spec: {
accessModes: ['ReadWriteOnce'],
storageClassName: 'longhorn',
resources: {
requests: {
storage: '5Gi',
async getAllPvcForApp(projectId: string, appId: string) {
const res = await k3s.core.listNamespacedPersistentVolumeClaim(projectId);
return res.body.items.filter((item) => item.metadata?.name?.startsWith(`pvc-${appId}`));
}
async deleteAllPvcOfApp(app: AppExtendedModel) {
const existingPvc = await this.getAllPvcForApp(app.projectId, app.id);
for (const pvc of existingPvc) {
try {
await k3s.core.deleteNamespacedPersistentVolumeClaim(pvc.metadata!.name!, app.projectId);
console.log(`Alle PVC-Konfigurationen für die App ${app.id} erfolgreich gelöscht.`);
} catch (error) {
console.error(`Fehler beim Löschen der PVC ${pvc.metadata!.name}:`, error);
}
}
}
async createOrUpdatePvc(app: AppExtendedModel) {
// Delete all existing PVCs for the app, need to be done because PVCs are mutable
await this.deleteAllPvcOfApp(app);
for (const pvcObj of app.appVolumes) {
const pvcName = `pvc-${app.id}-${pvcObj.id}`;
const pvcDefinition: V1PersistentVolumeClaim = {
apiVersion: 'v1',
kind: 'PersistentVolumeClaim',
metadata: {
name: pvcName,
namespace: app.projectId,
},
spec: {
accessModes: [pvcObj.accessMode],
storageClassName: 'longhorn',
resources: {
requests: {
storage: `${pvcObj.size}Gi`,
},
},
},
},
};
};
await k3s.core.createNamespacedPersistentVolumeClaim(app.projectId, pvcDefinition);
await k3s.core.createNamespacedPersistentVolumeClaim(app.projectId, pvcDefinition);
console.log(`PVC ${pvcName} für App ${app.id} erfolgreich erstellt.`);
}
}
/**