mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2026-02-11 05:59:23 -06:00
feat/add webhook ID to App model and implement webhook deployment functionality
This commit is contained in:
2
prisma/migrations/20241229143025_migration/migration.sql
Normal file
2
prisma/migrations/20241229143025_migration/migration.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "App" ADD COLUMN "webhookId" TEXT;
|
||||
@@ -149,6 +149,8 @@ model App {
|
||||
cpuReservation Int?
|
||||
cpuLimit Int?
|
||||
|
||||
webhookId String?
|
||||
|
||||
appDomains AppDomain[]
|
||||
appPorts AppPort[]
|
||||
appVolumes AppVolume[]
|
||||
|
||||
31
src/app/api/v1/webhook/deploy/route.ts
Normal file
31
src/app/api/v1/webhook/deploy/route.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import k3s from "@/server/adapter/kubernetes-api.adapter";
|
||||
import appService from "@/server/services/app.service";
|
||||
import deploymentService from "@/server/services/deployment.service";
|
||||
import { getAuthUserSession, simpleRoute } from "@/server/utils/action-wrapper.utils";
|
||||
import { Informer, V1Pod } from "@kubernetes/client-node";
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
// Prevents this route's response from being cached
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
return simpleRoute(async () => {
|
||||
const url = new URL(request.url);
|
||||
const searchParams = url.searchParams;
|
||||
|
||||
const { id } = z.object({
|
||||
id: z.string().min(1),
|
||||
}).parse({
|
||||
id: searchParams.get("id"),
|
||||
});
|
||||
|
||||
const app = await appService.getByWebhookId(id);
|
||||
await appService.buildAndDeploy(app.id, true);
|
||||
|
||||
return NextResponse.json({
|
||||
status: "success",
|
||||
body: "Deployment triggered.",
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import TerminalStreamed from "./overview/terminal-streamed";
|
||||
import { useEffect } from "react";
|
||||
import { useBreadcrumbs } from "@/frontend/states/zustand.states";
|
||||
import FileMount from "./volumes/file-mount";
|
||||
import WebhookDeploymentInfo from "./overview/webhook-deployment";
|
||||
|
||||
export default function AppTabs({
|
||||
app,
|
||||
@@ -45,6 +46,7 @@ export default function AppTabs({
|
||||
<MonitoringTab app={app} />
|
||||
<Logs app={app} />
|
||||
<BuildsTab app={app} />
|
||||
<WebhookDeploymentInfo app={app} />
|
||||
</TabsContent>
|
||||
<TabsContent value="general" className="space-y-4">
|
||||
<GeneralAppSource app={app} />
|
||||
|
||||
@@ -38,4 +38,10 @@ export const getRessourceDataApp = async (projectId: string, appId: string) =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
return await monitorAppService.getPodsForApp(projectId, appId);
|
||||
}) as Promise<ServerActionResult<unknown, PodsResourceInfoModel>>;
|
||||
}) as Promise<ServerActionResult<unknown, PodsResourceInfoModel>>;
|
||||
|
||||
export const createNewWebhookUrl = async (appId: string) =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
await appService.regenerateWebhookId(appId);
|
||||
});
|
||||
60
src/app/project/app/[appId]/overview/webhook-deployment.tsx
Normal file
60
src/app/project/app/[appId]/overview/webhook-deployment.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { AppExtendedModel } from "@/shared/model/app-extended.model";
|
||||
import { useEffect, useState } from "react";
|
||||
import { createNewWebhookUrl } from "./actions";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useConfirmDialog } from "@/frontend/states/zustand.states";
|
||||
import { Toast } from "@/frontend/utils/toast.utils";
|
||||
import { ClipboardCopy } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function WebhookDeploymentInfo({
|
||||
app
|
||||
}: {
|
||||
app: AppExtendedModel;
|
||||
}) {
|
||||
const { openConfirmDialog } = useConfirmDialog();
|
||||
const [webhookUrl, setWebhookUrl] = useState<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (app.webhookId) {
|
||||
const hostname = window.location.hostname;
|
||||
const port = [80, 443].includes(Number(window.location.port)) ? '' : `:${window.location.port}`;
|
||||
const protocol = window.location.protocol;
|
||||
setWebhookUrl(`${protocol}//${hostname}${port}/api/v1/webhook/deploy?id=${app.webhookId}`);
|
||||
}
|
||||
}, [app]);
|
||||
|
||||
const createNewWebhookUrlAsync = async () => {
|
||||
if (!await openConfirmDialog({
|
||||
title: 'Generate new Webhook URL',
|
||||
description: 'Are you sure you want to generate a new Webhook URL? The old URL will be invalidated.',
|
||||
okButton: 'Generate new URL'
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
await Toast.fromAction(() => createNewWebhookUrl(app.id), 'Webhook URL has been regenerated.');
|
||||
}
|
||||
|
||||
const copyWebhookUrl = () => {
|
||||
navigator.clipboard.writeText(webhookUrl!);
|
||||
toast.success('Webhook URL has been copied to clipboard.');
|
||||
}
|
||||
|
||||
return <>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Webhook Deployment</CardTitle>
|
||||
<CardDescription>Use this webhook URL to trigger deployments from external services (for example GitHub Actions or GitLab Pipelines).</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex gap-4">
|
||||
{webhookUrl && <Button className="flex-1 truncate" variant="secondary" onClick={copyWebhookUrl}>
|
||||
<span className="truncate">{webhookUrl}</span> <ClipboardCopy />
|
||||
</Button>}
|
||||
<Button onClick={createNewWebhookUrlAsync} variant={webhookUrl ? 'ghost' : 'secondary'}>{webhookUrl ? 'Generate new Webhook URL' : 'Enable Webhook deployments'}</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>;
|
||||
}
|
||||
@@ -118,6 +118,14 @@ class AppService {
|
||||
})(appId);
|
||||
}
|
||||
|
||||
async getByWebhookId(webhookId: string) {
|
||||
return await dataAccess.client.app.findFirstOrThrow({
|
||||
where: {
|
||||
webhookId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async save(item: Prisma.AppUncheckedCreateInput | Prisma.AppUncheckedUpdateInput, createDefaultPort = true) {
|
||||
let savedItem: App;
|
||||
try {
|
||||
@@ -151,6 +159,16 @@ class AppService {
|
||||
return savedItem;
|
||||
}
|
||||
|
||||
async regenerateWebhookId(appId: string) {
|
||||
const existingApp = await this.getById(appId);
|
||||
|
||||
const randomBytes = crypto.randomBytes(32).toString('hex');
|
||||
await this.save({
|
||||
...existingApp,
|
||||
webhookId: randomBytes
|
||||
});
|
||||
}
|
||||
|
||||
async saveDomain(domainToBeSaved: Prisma.AppDomainUncheckedCreateInput | Prisma.AppDomainUncheckedUpdateInput) {
|
||||
let savedItem: AppDomain;
|
||||
const existingApp = await this.getExtendedById(domainToBeSaved.appId as string);
|
||||
|
||||
@@ -22,6 +22,7 @@ export const AppModel = z.object({
|
||||
memoryLimit: z.number().int().nullish(),
|
||||
cpuReservation: z.number().int().nullish(),
|
||||
cpuLimit: z.number().int().nullish(),
|
||||
webhookId: z.string().nullish(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user