refactor: update template system with new configuration structure and processing

This commit is contained in:
Mauricio Siu
2025-03-01 03:11:29 -06:00
parent 49b37d531a
commit 9aff4bc10b
11 changed files with 585 additions and 434 deletions

View File

@@ -70,6 +70,38 @@ import Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";
interface TemplateData {
metadata: {
id: string;
name: string;
description: string;
version: string;
logo: string;
links: {
github: string;
website?: string;
docs?: string;
};
tags: string[];
};
variables: {
[key: string]: string;
};
config: {
domains: Array<{
serviceName: string;
port: number;
path?: string;
host?: string;
}>;
env: Record<string, string>;
mounts?: Array<{
filePath: string;
content: string;
}>;
};
}
interface Props {
projectId: string;
}
@@ -94,11 +126,13 @@ export const AddTemplate = ({ projectId }: Props) => {
data?.filter((template) => {
const matchesTags =
selectedTags.length === 0 ||
template.tags.some((tag) => selectedTags.includes(tag));
template.metadata.tags.some((tag) => selectedTags.includes(tag));
const matchesQuery =
query === "" ||
template.name.toLowerCase().includes(query.toLowerCase()) ||
template.description.toLowerCase().includes(query.toLowerCase());
template.metadata.name.toLowerCase().includes(query.toLowerCase()) ||
template.metadata.description
.toLowerCase()
.includes(query.toLowerCase());
return matchesTags && matchesQuery;
}) || [];
@@ -249,9 +283,9 @@ export const AddTemplate = ({ projectId }: Props) => {
: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6",
)}
>
{templates?.map((template, index) => (
{templates?.map((template) => (
<div
key={`template-${index}`}
key={template.metadata.id}
className={cn(
"flex flex-col border rounded-lg overflow-hidden relative",
viewMode === "icon" && "h-[200px]",
@@ -259,7 +293,7 @@ export const AddTemplate = ({ projectId }: Props) => {
)}
>
<Badge className="absolute top-2 right-2" variant="blue">
{template.version}
{template.metadata.version}
</Badge>
{/* Template Header */}
<div
@@ -269,21 +303,21 @@ export const AddTemplate = ({ projectId }: Props) => {
)}
>
<img
src={`/templates/${template.logo}`}
src={`/templates/${template.metadata.logo}`}
className={cn(
"object-contain",
viewMode === "detailed" ? "size-24" : "size-16",
)}
alt={template.name}
alt={template.metadata.name}
/>
<div className="flex flex-col items-center gap-2">
<span className="text-sm font-medium line-clamp-1">
{template.name}
{template.metadata.name}
</span>
{viewMode === "detailed" &&
template.tags.length > 0 && (
template.metadata.tags.length > 0 && (
<div className="flex flex-wrap justify-center gap-1.5">
{template.tags.map((tag) => (
{template.metadata.tags.map((tag) => (
<Badge
key={tag}
variant="green"
@@ -301,7 +335,7 @@ export const AddTemplate = ({ projectId }: Props) => {
{viewMode === "detailed" && (
<ScrollArea className="flex-1 p-6">
<div className="text-sm text-muted-foreground">
{template.description}
{template.metadata.description}
</div>
</ScrollArea>
)}
@@ -318,24 +352,24 @@ export const AddTemplate = ({ projectId }: Props) => {
{viewMode === "detailed" && (
<div className="flex gap-2">
<Link
href={template.links.github}
href={template.metadata.links.github}
target="_blank"
className="text-muted-foreground hover:text-foreground transition-colors"
>
<Github className="size-5" />
</Link>
{template.links.website && (
{template.metadata.links.website && (
<Link
href={template.links.website}
href={template.metadata.links.website}
target="_blank"
className="text-muted-foreground hover:text-foreground transition-colors"
>
<Globe className="size-5" />
</Link>
)}
{template.links.docs && (
{template.metadata.links.docs && (
<Link
href={template.links.docs}
href={template.metadata.links.docs}
target="_blank"
className="text-muted-foreground hover:text-foreground transition-colors"
>
@@ -364,8 +398,8 @@ export const AddTemplate = ({ projectId }: Props) => {
</AlertDialogTitle>
<AlertDialogDescription>
This will create an application from the{" "}
{template.name} template and add it to your
project.
{template.metadata.name} template and add it to
your project.
</AlertDialogDescription>
<div>
@@ -431,7 +465,7 @@ export const AddTemplate = ({ projectId }: Props) => {
const promise = mutateAsync({
projectId,
serverId: serverId || undefined,
id: template.id,
id: template.metadata.id,
});
toast.promise(promise, {
loading: "Setting up...",
@@ -440,10 +474,10 @@ export const AddTemplate = ({ projectId }: Props) => {
projectId,
});
setOpen(false);
return `${template.name} template created successfully`;
return `${template.metadata.name} template created successfully`;
},
error: (err) => {
return `An error ocurred deploying ${template.name} template`;
return `An error occurred deploying ${template.metadata.name} template`;
},
});
}}

View File

@@ -13,12 +13,13 @@ import {
import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
import { templates } from "@/templates/templates";
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
import { generatePassword } from "@/templates/utils";
import {
generatePassword,
loadTemplateModule,
readTemplateComposeFile,
} from "@/templates/utils";
import { fetchTemplatesList } from "@dokploy/server/templates/utils/github";
fetchTemplateFiles,
fetchTemplatesList,
processTemplate,
} from "@dokploy/server/templates/utils/github";
import { readTemplateComposeFile } from "@dokploy/server/templates/utils";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { dump } from "js-yaml";
@@ -56,6 +57,29 @@ import {
updateCompose,
} from "@dokploy/server";
import { z } from "zod";
import type { TemplateConfig } from "@dokploy/server/types/template";
// Add the template config schema
const templateConfigSchema = z.object({
variables: z.record(z.string()),
domains: z.array(
z.object({
serviceName: z.string(),
port: z.number(),
path: z.string().optional(),
host: z.string().optional(),
}),
),
env: z.record(z.string()),
mounts: z.array(
z.object({
filePath: z.string(),
content: z.string(),
}),
),
}) satisfies z.ZodType<TemplateConfig>;
export const composeRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateCompose)
@@ -374,7 +398,13 @@ export const composeRouter = createTRPCRouter({
return true;
}),
deployTemplate: protectedProcedure
.input(apiCreateComposeByTemplate)
.input(
z.object({
projectId: z.string(),
serverId: z.string().optional(),
id: z.string(),
}),
)
.mutation(async ({ ctx, input }) => {
if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
@@ -387,9 +417,7 @@ export const composeRouter = createTRPCRouter({
});
}
const composeFile = await readTemplateComposeFile(input.id);
const generate = await loadTemplateModule(input.id);
const template = await fetchTemplateFiles(input.id);
const admin = await findAdminById(ctx.user.adminId);
let serverIp = admin.serverIp || "127.0.0.1";
@@ -402,50 +430,60 @@ export const composeRouter = createTRPCRouter({
} else if (process.env.NODE_ENV === "development") {
serverIp = "127.0.0.1";
}
const projectName = slugify(`${project.name} ${input.id}`);
const { envs, mounts, domains } = await generate({
serverIp: serverIp || "",
projectName: projectName,
const generate = processTemplate(template.config, {
serverIp: serverIp,
projectName: project.name,
});
const compose = await createComposeByTemplate({
...input,
composeFile: composeFile,
env: envs?.join("\n"),
serverId: input.serverId,
name: input.id,
sourceType: "raw",
appName: `${projectName}-${generatePassword(6)}`,
isolatedDeployment: true,
});
console.log(generate.domains);
console.log(generate.envs);
console.log(generate.mounts);
if (ctx.user.rol === "user") {
await addNewService(ctx.user.authId, compose.composeId);
}
// const projectName = slugify(`${project.name} ${input.id}`);
// const { envs, mounts, domains } = await generate({
// serverIp: serverIp || "",
// projectName: projectName,
// });
if (mounts && mounts?.length > 0) {
for (const mount of mounts) {
await createMount({
filePath: mount.filePath,
mountPath: "",
content: mount.content,
serviceId: compose.composeId,
serviceType: "compose",
type: "file",
});
}
}
// const compose = await createComposeByTemplate({
// ...input,
// composeFile: composeFile,
// env: envs?.join("\n"),
// serverId: input.serverId,
// name: input.id,
// sourceType: "raw",
// appName: `${projectName}-${generatePassword(6)}`,
// isolatedDeployment: true,
// });
if (domains && domains?.length > 0) {
for (const domain of domains) {
await createDomain({
...domain,
domainType: "compose",
certificateType: "none",
composeId: compose.composeId,
});
}
}
// if (ctx.user.rol === "user") {
// await addNewService(ctx.user.authId, compose.composeId);
// }
// if (mounts && mounts?.length > 0) {
// for (const mount of mounts) {
// await createMount({
// filePath: mount.filePath,
// mountPath: "",
// content: mount.content,
// serviceId: compose.composeId,
// serviceType: "compose",
// type: "file",
// });
// }
// }
// if (domains && domains?.length > 0) {
// for (const domain of domains) {
// await createDomain({
// ...domain,
// domainType: "compose",
// certificateType: "none",
// composeId: compose.composeId,
// });
// }
// }
return null;
}),
@@ -465,7 +503,7 @@ export const composeRouter = createTRPCRouter({
}
// Fall back to local templates if GitHub fetch fails
return templates;
return [];
}),
getTags: protectedProcedure.query(async ({ input }) => {

View File

@@ -1,9 +0,0 @@
version: "3.8"
services:
pocketbase:
image: spectado/pocketbase:0.23.3
restart: unless-stopped
volumes:
- /etc/dokploy/templates/${HASH}/data:/pb_data
- /etc/dokploy/templates/${HASH}/public:/pb_public
- /etc/dokploy/templates/${HASH}/migrations:/pb_migrations

View File

@@ -1,22 +0,0 @@
import {
type DomainSchema,
type Schema,
type Template,
generateRandomDomain,
} from "../utils";
export function generate(schema: Schema): Template {
const mainDomain = generateRandomDomain(schema);
const domains: DomainSchema[] = [
{
host: mainDomain,
port: 80,
serviceName: "pocketbase",
},
];
return {
domains,
};
}

View File

@@ -47,21 +47,6 @@ export const templates: TemplateData[] = [
load: () => import("./supabase/index").then((m) => m.generate),
tags: ["database", "firebase", "postgres"],
},
{
id: "pocketbase",
name: "Pocketbase",
version: "v0.22.12",
description:
"Pocketbase is a self-hosted alternative to Firebase that allows you to build and host your own backend services.",
links: {
github: "https://github.com/pocketbase/pocketbase",
website: "https://pocketbase.io/",
docs: "https://pocketbase.io/docs/",
},
logo: "pocketbase.svg",
load: () => import("./pocketbase/index").then((m) => m.generate),
tags: ["database", "cms", "headless"],
},
{
id: "plausible",
name: "Plausible",

View File

@@ -28,7 +28,10 @@ export interface Template {
export const generateRandomDomain = ({
serverIp,
projectName,
}: Schema): string => {
}: {
serverIp: string;
projectName: string;
}): string => {
const hash = randomBytes(3).toString("hex");
const slugIp = serverIp.replaceAll(".", "-");
@@ -41,9 +44,15 @@ export const generateHash = (projectName: string, quantity = 3): string => {
};
export const generatePassword = (quantity = 16): string => {
return randomBytes(Math.ceil(quantity / 2))
.toString("hex")
.slice(0, quantity);
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let password = "";
for (let i = 0; i < quantity; i++) {
password += characters.charAt(
Math.floor(Math.random() * characters.length),
);
}
return password.toLowerCase();
};
export const generateBase64 = (bytes = 32): string => {