mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2026-01-01 17:20:14 -06:00
implemented pvc deployment
This commit is contained in:
18
prisma/migrations/20241106181617_migration/migration.sql
Normal file
18
prisma/migrations/20241106181617_migration/migration.sql
Normal 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;
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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>;
|
||||
@@ -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.`);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user