From 6df636ce210b62f1774389dba284c739267cd8b0 Mon Sep 17 00:00:00 2001 From: biersoeckli Date: Thu, 31 Oct 2024 15:12:33 +0000 Subject: [PATCH] first version of container build logs --- src/app/project/app/[tabName]/app-tabs.tsx | 10 ++- .../app/[tabName]/overview/builds-tab.tsx | 45 ++++++++++ src/app/project/app/[tabName]/page.tsx | 4 +- src/model/build-job.ts | 11 +++ src/server/services/build.service.ts | 85 +++++++++++++------ src/server/utils/cache-tag-generator.utils.ts | 4 + src/server/utils/string.utils.ts | 9 ++ 7 files changed, 139 insertions(+), 29 deletions(-) create mode 100644 src/app/project/app/[tabName]/overview/builds-tab.tsx create mode 100644 src/model/build-job.ts diff --git a/src/app/project/app/[tabName]/app-tabs.tsx b/src/app/project/app/[tabName]/app-tabs.tsx index 1fea22a..d34959d 100644 --- a/src/app/project/app/[tabName]/app-tabs.tsx +++ b/src/app/project/app/[tabName]/app-tabs.tsx @@ -9,13 +9,17 @@ import { App } from "@prisma/client"; import DomainsList from "./domains/domains"; import StorageList from "./storage/storages"; import { AppExtendedModel } from "@/model/app-extended.model"; +import { BuildJobModel } from "@/model/build-job"; +import BuildsTab from "./overview/builds-tab"; export default function AppTabs({ app, - tabName + tabName, + appBuilds }: { app: AppExtendedModel; tabName: string; + appBuilds: BuildJobModel[]; }) { const router = useRouter(); @@ -32,7 +36,9 @@ export default function AppTabs({ Domains Storage - Domains, Logs, etc. + + + diff --git a/src/app/project/app/[tabName]/overview/builds-tab.tsx b/src/app/project/app/[tabName]/overview/builds-tab.tsx new file mode 100644 index 0000000..961aeb7 --- /dev/null +++ b/src/app/project/app/[tabName]/overview/builds-tab.tsx @@ -0,0 +1,45 @@ +import { SimpleDataTable } from "@/components/custom/simple-data-table"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { formatDateTime } from "@/lib/format.utils"; +import { AppExtendedModel } from "@/model/app-extended.model"; +import { BuildJobModel } from "@/model/build-job"; + +export default function BuildsTab({ + app, + appBuilds +}: { + app: AppExtendedModel; + appBuilds: BuildJobModel[]; +}) { + + if (app.sourceType === 'container') { + return <>; + } + + return <> + + + Container Builds + This is an overview of the last container builds for this App. + + + formatDateTime(item.startTime)], + ]} + data={appBuilds} + hideSearchBar={true} + actionCol={(item) => + <> +
+
+
TODO BUTTON
+
+ } + /> + +
+
+ ; +} diff --git a/src/app/project/app/[tabName]/page.tsx b/src/app/project/app/[tabName]/page.tsx index f606306..35cb8ee 100644 --- a/src/app/project/app/[tabName]/page.tsx +++ b/src/app/project/app/[tabName]/page.tsx @@ -10,6 +10,7 @@ import { import PageTitle from "@/components/custom/page-title"; import AppTabs from "./app-tabs"; import AppActionButtons from "./app-action-buttons"; +import buildService from "@/server/services/build.service"; export default async function AppPage({ searchParams, @@ -24,6 +25,7 @@ export default async function AppPage({ return

Could not find app with id {appId}

} const app = await appService.getExtendedById(appId); + const builds = await buildService.getBuildsForApp(appId); return (
@@ -47,7 +49,7 @@ export default async function AppPage({ subtitle={`App ID: ${app.id}`}> - +
) } diff --git a/src/model/build-job.ts b/src/model/build-job.ts new file mode 100644 index 0000000..96915d3 --- /dev/null +++ b/src/model/build-job.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; + +export const buildJobSchemaZod = z.object({ + name: z.string(), + startTime: z.date(), + status: z.union([z.literal('UNKNOWN'), z.literal('RUNNING'), z.literal('FAILED'), z.literal('SUCCEEDED')]), +}); + +export type BuildJobModel = z.infer; + + diff --git a/src/server/services/build.service.ts b/src/server/services/build.service.ts index fecc566..d68f836 100644 --- a/src/server/services/build.service.ts +++ b/src/server/services/build.service.ts @@ -1,6 +1,10 @@ import { AppExtendedModel } from "@/model/app-extended.model"; import k3s from "../adapter/kubernetes-api.adapter"; -import { V1Deployment, V1Job } from "@kubernetes/client-node"; +import { V1Deployment, V1Job, V1JobStatus } from "@kubernetes/client-node"; +import { StringUtils } from "../utils/string.utils"; +import { revalidateTag, unstable_cache } from "next/cache"; +import { Tags } from "../utils/cache-tag-generator.utils"; +import { BuildJobModel } from "@/model/build-job"; const kanikoImage = "gcr.io/kaniko-project/executor:latest"; export const registryURL = "registry-svc.registry-and-build.svc.cluster.local" @@ -9,24 +13,25 @@ export const buildNamespace = "registry-and-build"; class BuildService { async buildApp(app: AppExtendedModel) { + const buildName = StringUtils.addRandomSuffix(StringUtils.toJobName(app.id)); const jobDefinition: V1Job = { apiVersion: "batch/v1", kind: "Job", metadata: { - name: `build-${app.id}`, + name: buildName, namespace: buildNamespace, }, spec: { - ttlSecondsAfterFinished: 100, + ttlSecondsAfterFinished: 2592000, // 30 days template: { spec: { containers: [ { - name: `build-${app.id}`, + name: buildName, image: kanikoImage, args: [`--dockerfile=${app.dockerfilePath}`, - `--context=${app.gitUrl!.replace("https://","git://")}#refs/heads/${app.gitBranch}`, - `--destination=${registryURL}/${app.id}`] + `--context=${app.gitUrl!.replace("https://", "git://")}#refs/heads/${app.gitBranch}`, + `--destination=${registryURL}/${app.id}`] }, ], restartPolicy: "Never", @@ -49,16 +54,39 @@ class BuildService { ]; } await k3s.batch.createNamespacedJob(buildNamespace, jobDefinition); - await this.waitForJobCompletion(buildNamespace, jobDefinition.metadata!.name!); + revalidateTag(Tags.appBuilds(app.id)); + this.waitForJobCompletion(jobDefinition.metadata!.name!) + .then(() => revalidateTag(Tags.appBuilds(app.id))) + .catch((err) => revalidateTag(Tags.appBuilds(app.id))); + + } - async waitForJobCompletion(namespace:string, jobName:string) { - const POLL_INTERVAL = 10000; // 10 seconds + async getBuildsForApp(appId: string) { + return await unstable_cache(async (appId: string) => { + const jobNamePrefix = StringUtils.toJobName(appId); + const jobs = await k3s.batch.listNamespacedJob(buildNamespace); + const jobsOfBuild = jobs.body.items.filter((job) => job.metadata?.name?.startsWith(jobNamePrefix)); + return jobsOfBuild.map((job) => { + return { + name: job.metadata?.name, + startTime: job.status?.startTime, + status: this.getJobStatusString(job.status), + } as BuildJobModel; + }); + }, + [Tags.appBuilds(appId)], { + tags: [Tags.appBuilds(appId)], + revalidate: 10, + })(appId); + } + async waitForJobCompletion(jobName: string) { + const POLL_INTERVAL = 10000; // 10 seconds return await new Promise((resolve, reject) => { const intervalId = setInterval(async () => { try { - const jobStatus = await this.getJobStatus(namespace, jobName); + const jobStatus = await this.getJobStatus(jobName); if (jobStatus === 'UNKNOWN') { console.log(`Job ${jobName} not found.`); clearInterval(intervalId); @@ -84,29 +112,34 @@ class BuildService { }); } - async getJobStatus(namespace:string, jobName:string): Promise<'UNKNOWN' | 'RUNNING' | 'FAILED' | 'SUCCEEDED'> { + async getJobStatus(jobName: string): Promise<'UNKNOWN' | 'RUNNING' | 'FAILED' | 'SUCCEEDED'> { try { - const response = await k3s.batch.readNamespacedJobStatus(jobName, namespace); - const job = response.body; - if (!job.status) { - return 'UNKNOWN'; - } - if ((job.status.active ?? 0) > 0) { - return 'RUNNING'; - } - if ((job.status.succeeded?? 0) > 0) { - return 'SUCCEEDED'; - } - - if ((job.status.failed ?? 0) > 0) { - return 'FAILED'; - } + const response = await k3s.batch.readNamespacedJobStatus(jobName, buildNamespace); + const status = response.body.status; + return this.getJobStatusString(status); } catch (err) { console.error(err); } return 'UNKNOWN'; } + getJobStatusString(status?: V1JobStatus) { + if (!status) { + return 'UNKNOWN'; + } + if ((status.active ?? 0) > 0) { + return 'RUNNING'; + } + if ((status.succeeded ?? 0) > 0) { + return 'SUCCEEDED'; + } + + if ((status.failed ?? 0) > 0) { + return 'FAILED'; + } + return 'UNKNOWN'; + } + } const buildService = new BuildService(); diff --git a/src/server/utils/cache-tag-generator.utils.ts b/src/server/utils/cache-tag-generator.utils.ts index 7efaaf4..14912bb 100644 --- a/src/server/utils/cache-tag-generator.utils.ts +++ b/src/server/utils/cache-tag-generator.utils.ts @@ -15,4 +15,8 @@ export class Tags { static app(appId: string) { return `app-${appId}`; } + + static appBuilds(appId: string) { + return `app-build-${appId}`; + } } \ No newline at end of file diff --git a/src/server/utils/string.utils.ts b/src/server/utils/string.utils.ts index f0a7ec0..c026e51 100644 --- a/src/server/utils/string.utils.ts +++ b/src/server/utils/string.utils.ts @@ -30,4 +30,13 @@ export class StringUtils { static toAppId(str: string): string { return `app-${StringUtils.toObjectId(str)}`; } + + static toJobName(appId: string) { + return `build-${appId}`; + } + + static addRandomSuffix(str: string): string { + const randomString = crypto.randomBytes(4).toString('hex'); + return `${str}-${randomString}`; + } } \ No newline at end of file