feat/add webhook ID to App model and implement webhook deployment functionality

This commit is contained in:
biersoeckli
2024-12-29 15:05:52 +00:00
parent cd3b6881b4
commit 2e714f0ab1
8 changed files with 123 additions and 1 deletions

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "App" ADD COLUMN "webhookId" TEXT;

View File

@@ -149,6 +149,8 @@ model App {
cpuReservation Int?
cpuLimit Int?
webhookId String?
appDomains AppDomain[]
appPorts AppPort[]
appVolumes AppVolume[]

View 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.",
});
});
}

View File

@@ -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} />

View File

@@ -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);
});

View 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>
</>;
}

View File

@@ -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);

View File

@@ -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(),
})