mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
refactor: update template system with new configuration structure and processing
This commit is contained in:
@@ -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`;
|
||||
},
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user