refactor: lint

This commit is contained in:
Mauricio Siu
2025-02-09 02:20:40 -06:00
parent 3e2cfe6eb8
commit a8f94540f9
7 changed files with 984 additions and 984 deletions

View File

@@ -14,8 +14,8 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config"; import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
import { RandomizeCompose } from "./randomize-compose";
import { RandomizeDeployable } from "./isolated-deployment"; import { RandomizeDeployable } from "./isolated-deployment";
import { RandomizeCompose } from "./randomize-compose";
import { ShowUtilities } from "./show-utilities"; import { ShowUtilities } from "./show-utilities";
interface Props { interface Props {

View File

@@ -1,6 +1,4 @@
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button";
import { IsolatedDeployment } from "./isolated-deployment";
import { RandomizeCompose } from "./randomize-compose";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -9,8 +7,10 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useState } from "react"; import { useState } from "react";
import { Button } from "@/components/ui/button"; import { IsolatedDeployment } from "./isolated-deployment";
import { RandomizeCompose } from "./randomize-compose";
interface Props { interface Props {
composeId: string; composeId: string;

View File

@@ -1,22 +1,22 @@
import { slugify } from "@/lib/slug"; import { slugify } from "@/lib/slug";
import { db } from "@/server/db"; import { db } from "@/server/db";
import { import {
apiCreateCompose, apiCreateCompose,
apiCreateComposeByTemplate, apiCreateComposeByTemplate,
apiDeleteCompose, apiDeleteCompose,
apiFetchServices, apiFetchServices,
apiFindCompose, apiFindCompose,
apiRandomizeCompose, apiRandomizeCompose,
apiUpdateCompose, apiUpdateCompose,
compose, compose,
} from "@/server/db/schema"; } from "@/server/db/schema";
import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup"; import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
import { templates } from "@/templates/templates"; import { templates } from "@/templates/templates";
import type { TemplatesKeys } from "@/templates/types/templates-data.type"; import type { TemplatesKeys } from "@/templates/types/templates-data.type";
import { import {
generatePassword, generatePassword,
loadTemplateModule, loadTemplateModule,
readTemplateComposeFile, readTemplateComposeFile,
} from "@/templates/utils"; } from "@/templates/utils";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
@@ -28,443 +28,443 @@ import { createTRPCRouter, protectedProcedure } from "../trpc";
import type { DeploymentJob } from "@/server/queues/queue-types"; import type { DeploymentJob } from "@/server/queues/queue-types";
import { deploy } from "@/server/utils/deploy"; import { deploy } from "@/server/utils/deploy";
import { import {
IS_CLOUD, IS_CLOUD,
addDomainToCompose, addDomainToCompose,
addNewService, addNewService,
checkServiceAccess, checkServiceAccess,
cloneCompose, cloneCompose,
cloneComposeRemote, cloneComposeRemote,
createCommand, createCommand,
createCompose, createCompose,
createComposeByTemplate, createComposeByTemplate,
createDomain, createDomain,
createMount, createMount,
findAdminById, findAdminById,
findComposeById, findComposeById,
findDomainsByComposeId, findDomainsByComposeId,
findProjectById, findProjectById,
findServerById, findServerById,
loadServices, loadServices,
randomizeComposeFile, randomizeComposeFile,
randomizeIsolatedDeploymentComposeFile, randomizeIsolatedDeploymentComposeFile,
removeCompose, removeCompose,
removeComposeDirectory, removeComposeDirectory,
removeDeploymentsByComposeId, removeDeploymentsByComposeId,
startCompose, startCompose,
stopCompose, stopCompose,
updateCompose, updateCompose,
} from "@dokploy/server"; } from "@dokploy/server";
export const composeRouter = createTRPCRouter({ export const composeRouter = createTRPCRouter({
create: protectedProcedure create: protectedProcedure
.input(apiCreateCompose) .input(apiCreateCompose)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
try { try {
if (ctx.user.rol === "user") { if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.projectId, "create"); await checkServiceAccess(ctx.user.authId, input.projectId, "create");
} }
if (IS_CLOUD && !input.serverId) { if (IS_CLOUD && !input.serverId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "You need to use a server to create a compose", message: "You need to use a server to create a compose",
}); });
} }
const project = await findProjectById(input.projectId); const project = await findProjectById(input.projectId);
if (project.adminId !== ctx.user.adminId) { if (project.adminId !== ctx.user.adminId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "You are not authorized to access this project", message: "You are not authorized to access this project",
}); });
} }
const newService = await createCompose(input); const newService = await createCompose(input);
if (ctx.user.rol === "user") { if (ctx.user.rol === "user") {
await addNewService(ctx.user.authId, newService.composeId); await addNewService(ctx.user.authId, newService.composeId);
} }
return newService; return newService;
} catch (error) { } catch (error) {
throw error; throw error;
} }
}), }),
one: protectedProcedure one: protectedProcedure
.input(apiFindCompose) .input(apiFindCompose)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
if (ctx.user.rol === "user") { if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.composeId, "access"); await checkServiceAccess(ctx.user.authId, input.composeId, "access");
} }
const compose = await findComposeById(input.composeId); const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) { if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "You are not authorized to access this compose", message: "You are not authorized to access this compose",
}); });
} }
return compose; return compose;
}), }),
update: protectedProcedure update: protectedProcedure
.input(apiUpdateCompose) .input(apiUpdateCompose)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId); const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) { if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "You are not authorized to update this compose", message: "You are not authorized to update this compose",
}); });
} }
return updateCompose(input.composeId, input); return updateCompose(input.composeId, input);
}), }),
delete: protectedProcedure delete: protectedProcedure
.input(apiDeleteCompose) .input(apiDeleteCompose)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
if (ctx.user.rol === "user") { if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.composeId, "delete"); await checkServiceAccess(ctx.user.authId, input.composeId, "delete");
} }
const composeResult = await findComposeById(input.composeId); const composeResult = await findComposeById(input.composeId);
if (composeResult.project.adminId !== ctx.user.adminId) { if (composeResult.project.adminId !== ctx.user.adminId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "You are not authorized to delete this compose", message: "You are not authorized to delete this compose",
}); });
} }
4; 4;
const result = await db const result = await db
.delete(compose) .delete(compose)
.where(eq(compose.composeId, input.composeId)) .where(eq(compose.composeId, input.composeId))
.returning(); .returning();
const cleanupOperations = [ const cleanupOperations = [
async () => await removeCompose(composeResult, input.deleteVolumes), async () => await removeCompose(composeResult, input.deleteVolumes),
async () => await removeDeploymentsByComposeId(composeResult), async () => await removeDeploymentsByComposeId(composeResult),
async () => await removeComposeDirectory(composeResult.appName), async () => await removeComposeDirectory(composeResult.appName),
]; ];
for (const operation of cleanupOperations) { for (const operation of cleanupOperations) {
try { try {
await operation(); await operation();
} catch (error) {} } catch (error) {}
} }
return result[0]; return result[0];
}), }),
cleanQueues: protectedProcedure cleanQueues: protectedProcedure
.input(apiFindCompose) .input(apiFindCompose)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId); const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) { if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "You are not authorized to clean this compose", message: "You are not authorized to clean this compose",
}); });
} }
await cleanQueuesByCompose(input.composeId); await cleanQueuesByCompose(input.composeId);
}), }),
loadServices: protectedProcedure loadServices: protectedProcedure
.input(apiFetchServices) .input(apiFetchServices)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId); const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) { if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "You are not authorized to load this compose", message: "You are not authorized to load this compose",
}); });
} }
return await loadServices(input.composeId, input.type); return await loadServices(input.composeId, input.type);
}), }),
fetchSourceType: protectedProcedure fetchSourceType: protectedProcedure
.input(apiFindCompose) .input(apiFindCompose)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
const compose = await findComposeById(input.composeId); const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) { if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "You are not authorized to fetch this compose", message: "You are not authorized to fetch this compose",
}); });
} }
if (compose.serverId) { if (compose.serverId) {
await cloneComposeRemote(compose); await cloneComposeRemote(compose);
} else { } else {
await cloneCompose(compose); await cloneCompose(compose);
} }
return compose.sourceType; return compose.sourceType;
} catch (err) { } catch (err) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Error fetching source type", message: "Error fetching source type",
cause: err, cause: err,
}); });
} }
}), }),
randomizeCompose: protectedProcedure randomizeCompose: protectedProcedure
.input(apiRandomizeCompose) .input(apiRandomizeCompose)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId); const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) { if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "You are not authorized to randomize this compose", message: "You are not authorized to randomize this compose",
}); });
} }
return await randomizeComposeFile(input.composeId, input.suffix); return await randomizeComposeFile(input.composeId, input.suffix);
}), }),
isolatedDeployment: protectedProcedure isolatedDeployment: protectedProcedure
.input(apiRandomizeCompose) .input(apiRandomizeCompose)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId); const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) { if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "You are not authorized to randomize this compose", message: "You are not authorized to randomize this compose",
}); });
} }
return await randomizeIsolatedDeploymentComposeFile( return await randomizeIsolatedDeploymentComposeFile(
input.composeId, input.composeId,
input.suffix input.suffix,
); );
}), }),
getConvertedCompose: protectedProcedure getConvertedCompose: protectedProcedure
.input(apiFindCompose) .input(apiFindCompose)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId); const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) { if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "You are not authorized to get this compose", message: "You are not authorized to get this compose",
}); });
} }
const domains = await findDomainsByComposeId(input.composeId); const domains = await findDomainsByComposeId(input.composeId);
const composeFile = await addDomainToCompose(compose, domains); const composeFile = await addDomainToCompose(compose, domains);
return dump(composeFile, { return dump(composeFile, {
lineWidth: 1000, lineWidth: 1000,
}); });
}), }),
deploy: protectedProcedure deploy: protectedProcedure
.input(apiFindCompose) .input(apiFindCompose)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId); const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) { if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "You are not authorized to deploy this compose", message: "You are not authorized to deploy this compose",
}); });
} }
const jobData: DeploymentJob = { const jobData: DeploymentJob = {
composeId: input.composeId, composeId: input.composeId,
titleLog: "Manual deployment", titleLog: "Manual deployment",
type: "deploy", type: "deploy",
applicationType: "compose", applicationType: "compose",
descriptionLog: "", descriptionLog: "",
server: !!compose.serverId, server: !!compose.serverId,
}; };
if (IS_CLOUD && compose.serverId) { if (IS_CLOUD && compose.serverId) {
jobData.serverId = compose.serverId; jobData.serverId = compose.serverId;
await deploy(jobData); await deploy(jobData);
return true; return true;
} }
await myQueue.add( await myQueue.add(
"deployments", "deployments",
{ ...jobData }, { ...jobData },
{ {
removeOnComplete: true, removeOnComplete: true,
removeOnFail: true, removeOnFail: true,
} },
); );
}), }),
redeploy: protectedProcedure redeploy: protectedProcedure
.input(apiFindCompose) .input(apiFindCompose)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId); const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) { if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "You are not authorized to redeploy this compose", message: "You are not authorized to redeploy this compose",
}); });
} }
const jobData: DeploymentJob = { const jobData: DeploymentJob = {
composeId: input.composeId, composeId: input.composeId,
titleLog: "Rebuild deployment", titleLog: "Rebuild deployment",
type: "redeploy", type: "redeploy",
applicationType: "compose", applicationType: "compose",
descriptionLog: "", descriptionLog: "",
server: !!compose.serverId, server: !!compose.serverId,
}; };
if (IS_CLOUD && compose.serverId) { if (IS_CLOUD && compose.serverId) {
jobData.serverId = compose.serverId; jobData.serverId = compose.serverId;
await deploy(jobData); await deploy(jobData);
return true; return true;
} }
await myQueue.add( await myQueue.add(
"deployments", "deployments",
{ ...jobData }, { ...jobData },
{ {
removeOnComplete: true, removeOnComplete: true,
removeOnFail: true, removeOnFail: true,
} },
); );
}), }),
stop: protectedProcedure stop: protectedProcedure
.input(apiFindCompose) .input(apiFindCompose)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId); const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) { if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "You are not authorized to stop this compose", message: "You are not authorized to stop this compose",
}); });
} }
await stopCompose(input.composeId); await stopCompose(input.composeId);
return true; return true;
}), }),
start: protectedProcedure start: protectedProcedure
.input(apiFindCompose) .input(apiFindCompose)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId); const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) { if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "You are not authorized to stop this compose", message: "You are not authorized to stop this compose",
}); });
} }
await startCompose(input.composeId); await startCompose(input.composeId);
return true; return true;
}), }),
getDefaultCommand: protectedProcedure getDefaultCommand: protectedProcedure
.input(apiFindCompose) .input(apiFindCompose)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId); const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) { if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "You are not authorized to get this compose", message: "You are not authorized to get this compose",
}); });
} }
const command = createCommand(compose); const command = createCommand(compose);
return `docker ${command}`; return `docker ${command}`;
}), }),
refreshToken: protectedProcedure refreshToken: protectedProcedure
.input(apiFindCompose) .input(apiFindCompose)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId); const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) { if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "You are not authorized to refresh this compose", message: "You are not authorized to refresh this compose",
}); });
} }
await updateCompose(input.composeId, { await updateCompose(input.composeId, {
refreshToken: nanoid(), refreshToken: nanoid(),
}); });
return true; return true;
}), }),
deployTemplate: protectedProcedure deployTemplate: protectedProcedure
.input(apiCreateComposeByTemplate) .input(apiCreateComposeByTemplate)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
if (ctx.user.rol === "user") { if (ctx.user.rol === "user") {
await checkServiceAccess(ctx.user.authId, input.projectId, "create"); await checkServiceAccess(ctx.user.authId, input.projectId, "create");
} }
if (IS_CLOUD && !input.serverId) { if (IS_CLOUD && !input.serverId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "You need to use a server to create a compose", message: "You need to use a server to create a compose",
}); });
} }
const composeFile = await readTemplateComposeFile(input.id); const composeFile = await readTemplateComposeFile(input.id);
const generate = await loadTemplateModule(input.id as TemplatesKeys); const generate = await loadTemplateModule(input.id as TemplatesKeys);
const admin = await findAdminById(ctx.user.adminId); const admin = await findAdminById(ctx.user.adminId);
let serverIp = admin.serverIp || "127.0.0.1"; let serverIp = admin.serverIp || "127.0.0.1";
const project = await findProjectById(input.projectId); const project = await findProjectById(input.projectId);
if (input.serverId) { if (input.serverId) {
const server = await findServerById(input.serverId); const server = await findServerById(input.serverId);
serverIp = server.ipAddress; serverIp = server.ipAddress;
} else if (process.env.NODE_ENV === "development") { } else if (process.env.NODE_ENV === "development") {
serverIp = "127.0.0.1"; serverIp = "127.0.0.1";
} }
const projectName = slugify(`${project.name} ${input.id}`); const projectName = slugify(`${project.name} ${input.id}`);
const { envs, mounts, domains } = generate({ const { envs, mounts, domains } = generate({
serverIp: serverIp || "", serverIp: serverIp || "",
projectName: projectName, projectName: projectName,
}); });
const compose = await createComposeByTemplate({ const compose = await createComposeByTemplate({
...input, ...input,
composeFile: composeFile, composeFile: composeFile,
env: envs?.join("\n"), env: envs?.join("\n"),
serverId: input.serverId, serverId: input.serverId,
name: input.id, name: input.id,
sourceType: "raw", sourceType: "raw",
appName: `${projectName}-${generatePassword(6)}`, appName: `${projectName}-${generatePassword(6)}`,
}); });
if (ctx.user.rol === "user") { if (ctx.user.rol === "user") {
await addNewService(ctx.user.authId, compose.composeId); await addNewService(ctx.user.authId, compose.composeId);
} }
if (mounts && mounts?.length > 0) { if (mounts && mounts?.length > 0) {
for (const mount of mounts) { for (const mount of mounts) {
await createMount({ await createMount({
filePath: mount.filePath, filePath: mount.filePath,
mountPath: "", mountPath: "",
content: mount.content, content: mount.content,
serviceId: compose.composeId, serviceId: compose.composeId,
serviceType: "compose", serviceType: "compose",
type: "file", type: "file",
}); });
} }
} }
if (domains && domains?.length > 0) { if (domains && domains?.length > 0) {
for (const domain of domains) { for (const domain of domains) {
await createDomain({ await createDomain({
...domain, ...domain,
domainType: "compose", domainType: "compose",
certificateType: "none", certificateType: "none",
composeId: compose.composeId, composeId: compose.composeId,
}); });
} }
} }
return null; return null;
}), }),
templates: protectedProcedure.query(async () => { templates: protectedProcedure.query(async () => {
const templatesData = templates.map((t) => ({ const templatesData = templates.map((t) => ({
name: t.name, name: t.name,
description: t.description, description: t.description,
id: t.id, id: t.id,
links: t.links, links: t.links,
tags: t.tags, tags: t.tags,
logo: t.logo, logo: t.logo,
version: t.version, version: t.version,
})); }));
return templatesData; return templatesData;
}), }),
getTags: protectedProcedure.query(async ({ input }) => { getTags: protectedProcedure.query(async ({ input }) => {
const allTags = templates.flatMap((template) => template.tags); const allTags = templates.flatMap((template) => template.tags);
const uniqueTags = _.uniq(allTags); const uniqueTags = _.uniq(allTags);
return uniqueTags; return uniqueTags;
}), }),
}); });

View File

@@ -16,170 +16,170 @@ import { sshKeys } from "./ssh-key";
import { generateAppName } from "./utils"; import { generateAppName } from "./utils";
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [ export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
"git", "git",
"github", "github",
"gitlab", "gitlab",
"bitbucket", "bitbucket",
"raw", "raw",
]); ]);
export const composeType = pgEnum("composeType", ["docker-compose", "stack"]); export const composeType = pgEnum("composeType", ["docker-compose", "stack"]);
export const compose = pgTable("compose", { export const compose = pgTable("compose", {
composeId: text("composeId") composeId: text("composeId")
.notNull() .notNull()
.primaryKey() .primaryKey()
.$defaultFn(() => nanoid()), .$defaultFn(() => nanoid()),
name: text("name").notNull(), name: text("name").notNull(),
appName: text("appName") appName: text("appName")
.notNull() .notNull()
.$defaultFn(() => generateAppName("compose")), .$defaultFn(() => generateAppName("compose")),
description: text("description"), description: text("description"),
env: text("env"), env: text("env"),
composeFile: text("composeFile").notNull().default(""), composeFile: text("composeFile").notNull().default(""),
refreshToken: text("refreshToken").$defaultFn(() => nanoid()), refreshToken: text("refreshToken").$defaultFn(() => nanoid()),
sourceType: sourceTypeCompose("sourceType").notNull().default("github"), sourceType: sourceTypeCompose("sourceType").notNull().default("github"),
composeType: composeType("composeType").notNull().default("docker-compose"), composeType: composeType("composeType").notNull().default("docker-compose"),
// Github // Github
repository: text("repository"), repository: text("repository"),
owner: text("owner"), owner: text("owner"),
branch: text("branch"), branch: text("branch"),
autoDeploy: boolean("autoDeploy").$defaultFn(() => true), autoDeploy: boolean("autoDeploy").$defaultFn(() => true),
// Gitlab // Gitlab
gitlabProjectId: integer("gitlabProjectId"), gitlabProjectId: integer("gitlabProjectId"),
gitlabRepository: text("gitlabRepository"), gitlabRepository: text("gitlabRepository"),
gitlabOwner: text("gitlabOwner"), gitlabOwner: text("gitlabOwner"),
gitlabBranch: text("gitlabBranch"), gitlabBranch: text("gitlabBranch"),
gitlabPathNamespace: text("gitlabPathNamespace"), gitlabPathNamespace: text("gitlabPathNamespace"),
// Bitbucket // Bitbucket
bitbucketRepository: text("bitbucketRepository"), bitbucketRepository: text("bitbucketRepository"),
bitbucketOwner: text("bitbucketOwner"), bitbucketOwner: text("bitbucketOwner"),
bitbucketBranch: text("bitbucketBranch"), bitbucketBranch: text("bitbucketBranch"),
// Git // Git
customGitUrl: text("customGitUrl"), customGitUrl: text("customGitUrl"),
customGitBranch: text("customGitBranch"), customGitBranch: text("customGitBranch"),
customGitSSHKeyId: text("customGitSSHKeyId").references( customGitSSHKeyId: text("customGitSSHKeyId").references(
() => sshKeys.sshKeyId, () => sshKeys.sshKeyId,
{ {
onDelete: "set null", onDelete: "set null",
} },
), ),
command: text("command").notNull().default(""), command: text("command").notNull().default(""),
// //
composePath: text("composePath").notNull().default("./docker-compose.yml"), composePath: text("composePath").notNull().default("./docker-compose.yml"),
suffix: text("suffix").notNull().default(""), suffix: text("suffix").notNull().default(""),
randomize: boolean("randomize").notNull().default(false), randomize: boolean("randomize").notNull().default(false),
isolatedDeployment: boolean("isolatedDeployment").notNull().default(false), isolatedDeployment: boolean("isolatedDeployment").notNull().default(false),
composeStatus: applicationStatus("composeStatus").notNull().default("idle"), composeStatus: applicationStatus("composeStatus").notNull().default("idle"),
projectId: text("projectId") projectId: text("projectId")
.notNull() .notNull()
.references(() => projects.projectId, { onDelete: "cascade" }), .references(() => projects.projectId, { onDelete: "cascade" }),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
.$defaultFn(() => new Date().toISOString()), .$defaultFn(() => new Date().toISOString()),
githubId: text("githubId").references(() => github.githubId, { githubId: text("githubId").references(() => github.githubId, {
onDelete: "set null", onDelete: "set null",
}), }),
gitlabId: text("gitlabId").references(() => gitlab.gitlabId, { gitlabId: text("gitlabId").references(() => gitlab.gitlabId, {
onDelete: "set null", onDelete: "set null",
}), }),
bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, { bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, {
onDelete: "set null", onDelete: "set null",
}), }),
serverId: text("serverId").references(() => server.serverId, { serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade", onDelete: "cascade",
}), }),
}); });
export const composeRelations = relations(compose, ({ one, many }) => ({ export const composeRelations = relations(compose, ({ one, many }) => ({
project: one(projects, { project: one(projects, {
fields: [compose.projectId], fields: [compose.projectId],
references: [projects.projectId], references: [projects.projectId],
}), }),
deployments: many(deployments), deployments: many(deployments),
mounts: many(mounts), mounts: many(mounts),
customGitSSHKey: one(sshKeys, { customGitSSHKey: one(sshKeys, {
fields: [compose.customGitSSHKeyId], fields: [compose.customGitSSHKeyId],
references: [sshKeys.sshKeyId], references: [sshKeys.sshKeyId],
}), }),
domains: many(domains), domains: many(domains),
github: one(github, { github: one(github, {
fields: [compose.githubId], fields: [compose.githubId],
references: [github.githubId], references: [github.githubId],
}), }),
gitlab: one(gitlab, { gitlab: one(gitlab, {
fields: [compose.gitlabId], fields: [compose.gitlabId],
references: [gitlab.gitlabId], references: [gitlab.gitlabId],
}), }),
bitbucket: one(bitbucket, { bitbucket: one(bitbucket, {
fields: [compose.bitbucketId], fields: [compose.bitbucketId],
references: [bitbucket.bitbucketId], references: [bitbucket.bitbucketId],
}), }),
server: one(server, { server: one(server, {
fields: [compose.serverId], fields: [compose.serverId],
references: [server.serverId], references: [server.serverId],
}), }),
})); }));
const createSchema = createInsertSchema(compose, { const createSchema = createInsertSchema(compose, {
name: z.string().min(1), name: z.string().min(1),
description: z.string(), description: z.string(),
env: z.string().optional(), env: z.string().optional(),
composeFile: z.string().min(1), composeFile: z.string().min(1),
projectId: z.string(), projectId: z.string(),
customGitSSHKeyId: z.string().optional(), customGitSSHKeyId: z.string().optional(),
command: z.string().optional(), command: z.string().optional(),
composePath: z.string().min(1), composePath: z.string().min(1),
composeType: z.enum(["docker-compose", "stack"]).optional(), composeType: z.enum(["docker-compose", "stack"]).optional(),
}); });
export const apiCreateCompose = createSchema.pick({ export const apiCreateCompose = createSchema.pick({
name: true, name: true,
description: true, description: true,
projectId: true, projectId: true,
composeType: true, composeType: true,
appName: true, appName: true,
serverId: true, serverId: true,
}); });
export const apiCreateComposeByTemplate = createSchema export const apiCreateComposeByTemplate = createSchema
.pick({ .pick({
projectId: true, projectId: true,
}) })
.extend({ .extend({
id: z.string().min(1), id: z.string().min(1),
serverId: z.string().optional(), serverId: z.string().optional(),
}); });
export const apiFindCompose = z.object({ export const apiFindCompose = z.object({
composeId: z.string().min(1), composeId: z.string().min(1),
}); });
export const apiDeleteCompose = z.object({ export const apiDeleteCompose = z.object({
composeId: z.string().min(1), composeId: z.string().min(1),
deleteVolumes: z.boolean(), deleteVolumes: z.boolean(),
}); });
export const apiFetchServices = z.object({ export const apiFetchServices = z.object({
composeId: z.string().min(1), composeId: z.string().min(1),
type: z.enum(["fetch", "cache"]).optional().default("cache"), type: z.enum(["fetch", "cache"]).optional().default("cache"),
}); });
export const apiUpdateCompose = createSchema export const apiUpdateCompose = createSchema
.partial() .partial()
.extend({ .extend({
composeId: z.string(), composeId: z.string(),
composeFile: z.string().optional(), composeFile: z.string().optional(),
command: z.string().optional(), command: z.string().optional(),
}) })
.omit({ serverId: true }); .omit({ serverId: true });
export const apiRandomizeCompose = createSchema export const apiRandomizeCompose = createSchema
.pick({ .pick({
composeId: true, composeId: true,
}) })
.extend({ .extend({
suffix: z.string().optional(), suffix: z.string().optional(),
composeId: z.string().min(1), composeId: z.string().min(1),
}); });

View File

@@ -1,117 +1,117 @@
import { import {
createWriteStream, createWriteStream,
existsSync, existsSync,
mkdirSync, mkdirSync,
readFileSync, readFileSync,
writeFileSync, writeFileSync,
} from "node:fs"; } from "node:fs";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import type { InferResultType } from "@dokploy/server/types/with"; import type { InferResultType } from "@dokploy/server/types/with";
import boxen from "boxen"; import boxen from "boxen";
import { import {
writeDomainsToCompose, writeDomainsToCompose,
writeDomainsToComposeRemote, writeDomainsToComposeRemote,
} from "../docker/domain"; } from "../docker/domain";
import { import {
encodeBase64, encodeBase64,
getEnviromentVariablesObject, getEnviromentVariablesObject,
prepareEnvironmentVariables, prepareEnvironmentVariables,
} from "../docker/utils"; } from "../docker/utils";
import { execAsync, execAsyncRemote } from "../process/execAsync"; import { execAsync, execAsyncRemote } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync"; import { spawnAsync } from "../process/spawnAsync";
export type ComposeNested = InferResultType< export type ComposeNested = InferResultType<
"compose", "compose",
{ project: true; mounts: true; domains: true } { project: true; mounts: true; domains: true }
>; >;
export const buildCompose = async (compose: ComposeNested, logPath: string) => { export const buildCompose = async (compose: ComposeNested, logPath: string) => {
const writeStream = createWriteStream(logPath, { flags: "a" }); const writeStream = createWriteStream(logPath, { flags: "a" });
const { sourceType, appName, mounts, composeType, domains } = compose; const { sourceType, appName, mounts, composeType, domains } = compose;
try { try {
const { COMPOSE_PATH } = paths(); const { COMPOSE_PATH } = paths();
const command = createCommand(compose); const command = createCommand(compose);
await writeDomainsToCompose(compose, domains); await writeDomainsToCompose(compose, domains);
createEnvFile(compose); createEnvFile(compose);
if (compose.isolatedDeployment) { if (compose.isolatedDeployment) {
await execAsync( await execAsync(
`docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create --attachable ${compose.appName}` `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create --attachable ${compose.appName}`,
); );
} }
const logContent = ` const logContent = `
App Name: ${appName} App Name: ${appName}
Build Compose 🐳 Build Compose 🐳
Detected: ${mounts.length} mounts 📂 Detected: ${mounts.length} mounts 📂
Command: docker ${command} Command: docker ${command}
Source Type: docker ${sourceType} Source Type: docker ${sourceType}
Compose Type: ${composeType}`; Compose Type: ${composeType}`;
const logBox = boxen(logContent, { const logBox = boxen(logContent, {
padding: { padding: {
left: 1, left: 1,
right: 1, right: 1,
bottom: 1, bottom: 1,
}, },
width: 80, width: 80,
borderStyle: "double", borderStyle: "double",
}); });
writeStream.write(`\n${logBox}\n`); writeStream.write(`\n${logBox}\n`);
const projectPath = join(COMPOSE_PATH, compose.appName, "code"); const projectPath = join(COMPOSE_PATH, compose.appName, "code");
await spawnAsync( await spawnAsync(
"docker", "docker",
[...command.split(" ")], [...command.split(" ")],
(data) => { (data) => {
if (writeStream.writable) { if (writeStream.writable) {
writeStream.write(data.toString()); writeStream.write(data.toString());
} }
}, },
{ {
cwd: projectPath, cwd: projectPath,
env: { env: {
NODE_ENV: process.env.NODE_ENV, NODE_ENV: process.env.NODE_ENV,
PATH: process.env.PATH, PATH: process.env.PATH,
...(composeType === "stack" && { ...(composeType === "stack" && {
...getEnviromentVariablesObject(compose.env, compose.project.env), ...getEnviromentVariablesObject(compose.env, compose.project.env),
}), }),
}, },
} },
); );
if (compose.isolatedDeployment) { if (compose.isolatedDeployment) {
await execAsync( await execAsync(
`docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1` `docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1`,
); );
} }
writeStream.write("Docker Compose Deployed: ✅"); writeStream.write("Docker Compose Deployed: ✅");
} catch (error) { } catch (error) {
writeStream.write(`Error ❌ ${(error as Error).message}`); writeStream.write(`Error ❌ ${(error as Error).message}`);
throw error; throw error;
} finally { } finally {
writeStream.end(); writeStream.end();
} }
}; };
export const getBuildComposeCommand = async ( export const getBuildComposeCommand = async (
compose: ComposeNested, compose: ComposeNested,
logPath: string logPath: string,
) => { ) => {
const { COMPOSE_PATH } = paths(true); const { COMPOSE_PATH } = paths(true);
const { sourceType, appName, mounts, composeType, domains, composePath } = const { sourceType, appName, mounts, composeType, domains, composePath } =
compose; compose;
const command = createCommand(compose); const command = createCommand(compose);
const envCommand = getCreateEnvFileCommand(compose); const envCommand = getCreateEnvFileCommand(compose);
const projectPath = join(COMPOSE_PATH, compose.appName, "code"); const projectPath = join(COMPOSE_PATH, compose.appName, "code");
const exportEnvCommand = getExportEnvCommand(compose); const exportEnvCommand = getExportEnvCommand(compose);
const newCompose = await writeDomainsToComposeRemote( const newCompose = await writeDomainsToComposeRemote(
compose, compose,
domains, domains,
logPath logPath,
); );
const logContent = ` const logContent = `
App Name: ${appName} App Name: ${appName}
Build Compose 🐳 Build Compose 🐳
Detected: ${mounts.length} mounts 📂 Detected: ${mounts.length} mounts 📂
@@ -119,17 +119,17 @@ Command: docker ${command}
Source Type: docker ${sourceType} Source Type: docker ${sourceType}
Compose Type: ${composeType}`; Compose Type: ${composeType}`;
const logBox = boxen(logContent, { const logBox = boxen(logContent, {
padding: { padding: {
left: 1, left: 1,
right: 1, right: 1,
bottom: 1, bottom: 1,
}, },
width: 80, width: 80,
borderStyle: "double", borderStyle: "double",
}); });
const bashCommand = ` const bashCommand = `
set -e set -e
{ {
echo "${logBox}" >> "${logPath}" echo "${logBox}" >> "${logPath}"
@@ -152,106 +152,106 @@ Compose Type: ${composeType} ✅`;
} }
`; `;
return await execAsyncRemote(compose.serverId, bashCommand); return await execAsyncRemote(compose.serverId, bashCommand);
}; };
const sanitizeCommand = (command: string) => { const sanitizeCommand = (command: string) => {
const sanitizedCommand = command.trim(); const sanitizedCommand = command.trim();
const parts = sanitizedCommand.split(/\s+/); const parts = sanitizedCommand.split(/\s+/);
const restCommand = parts.map((arg) => arg.replace(/^"(.*)"$/, "$1")); const restCommand = parts.map((arg) => arg.replace(/^"(.*)"$/, "$1"));
return restCommand.join(" "); return restCommand.join(" ");
}; };
export const createCommand = (compose: ComposeNested) => { export const createCommand = (compose: ComposeNested) => {
const { composeType, appName, sourceType } = compose; const { composeType, appName, sourceType } = compose;
if (compose.command) { if (compose.command) {
return `${sanitizeCommand(compose.command)}`; return `${sanitizeCommand(compose.command)}`;
} }
const path = const path =
sourceType === "raw" ? "docker-compose.yml" : compose.composePath; sourceType === "raw" ? "docker-compose.yml" : compose.composePath;
let command = ""; let command = "";
if (composeType === "docker-compose") { if (composeType === "docker-compose") {
command = `compose -p ${appName} -f ${path} up -d --build --remove-orphans`; command = `compose -p ${appName} -f ${path} up -d --build --remove-orphans`;
} else if (composeType === "stack") { } else if (composeType === "stack") {
command = `stack deploy -c ${path} ${appName} --prune`; command = `stack deploy -c ${path} ${appName} --prune`;
} }
return command; return command;
}; };
const createEnvFile = (compose: ComposeNested) => { const createEnvFile = (compose: ComposeNested) => {
const { COMPOSE_PATH } = paths(); const { COMPOSE_PATH } = paths();
const { env, composePath, appName } = compose; const { env, composePath, appName } = compose;
const composeFilePath = const composeFilePath =
join(COMPOSE_PATH, appName, "code", composePath) || join(COMPOSE_PATH, appName, "code", composePath) ||
join(COMPOSE_PATH, appName, "code", "docker-compose.yml"); join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
const envFilePath = join(dirname(composeFilePath), ".env"); const envFilePath = join(dirname(composeFilePath), ".env");
let envContent = env || ""; let envContent = env || "";
if (!envContent.includes("DOCKER_CONFIG")) { if (!envContent.includes("DOCKER_CONFIG")) {
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json"; envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
} }
if (compose.randomize) { if (compose.randomize) {
envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`; envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`;
} }
const envFileContent = prepareEnvironmentVariables( const envFileContent = prepareEnvironmentVariables(
envContent, envContent,
compose.project.env compose.project.env,
).join("\n"); ).join("\n");
if (!existsSync(dirname(envFilePath))) { if (!existsSync(dirname(envFilePath))) {
mkdirSync(dirname(envFilePath), { recursive: true }); mkdirSync(dirname(envFilePath), { recursive: true });
} }
writeFileSync(envFilePath, envFileContent); writeFileSync(envFilePath, envFileContent);
}; };
export const getCreateEnvFileCommand = (compose: ComposeNested) => { export const getCreateEnvFileCommand = (compose: ComposeNested) => {
const { COMPOSE_PATH } = paths(true); const { COMPOSE_PATH } = paths(true);
const { env, composePath, appName } = compose; const { env, composePath, appName } = compose;
const composeFilePath = const composeFilePath =
join(COMPOSE_PATH, appName, "code", composePath) || join(COMPOSE_PATH, appName, "code", composePath) ||
join(COMPOSE_PATH, appName, "code", "docker-compose.yml"); join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
const envFilePath = join(dirname(composeFilePath), ".env"); const envFilePath = join(dirname(composeFilePath), ".env");
let envContent = env || ""; let envContent = env || "";
if (!envContent.includes("DOCKER_CONFIG")) { if (!envContent.includes("DOCKER_CONFIG")) {
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json"; envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
} }
if (compose.randomize) { if (compose.randomize) {
envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`; envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`;
} }
const envFileContent = prepareEnvironmentVariables( const envFileContent = prepareEnvironmentVariables(
envContent, envContent,
compose.project.env compose.project.env,
).join("\n"); ).join("\n");
const encodedContent = encodeBase64(envFileContent); const encodedContent = encodeBase64(envFileContent);
return ` return `
touch ${envFilePath}; touch ${envFilePath};
echo "${encodedContent}" | base64 -d > "${envFilePath}"; echo "${encodedContent}" | base64 -d > "${envFilePath}";
`; `;
}; };
const getExportEnvCommand = (compose: ComposeNested) => { const getExportEnvCommand = (compose: ComposeNested) => {
if (compose.composeType !== "stack") return ""; if (compose.composeType !== "stack") return "";
const envVars = getEnviromentVariablesObject( const envVars = getEnviromentVariablesObject(
compose.env, compose.env,
compose.project.env compose.project.env,
); );
const exports = Object.entries(envVars) const exports = Object.entries(envVars)
.map(([key, value]) => `export ${key}=${JSON.stringify(value)}`) .map(([key, value]) => `export ${key}=${JSON.stringify(value)}`)
.join("\n"); .join("\n");
return exports ? `\n# Export environment variables\n${exports}\n` : ""; return exports ? `\n# Export environment variables\n${exports}\n` : "";
}; };

View File

@@ -6,41 +6,41 @@ import { addSuffixToAllVolumes } from "./compose/volume";
import type { ComposeSpecification } from "./types"; import type { ComposeSpecification } from "./types";
export const addAppNameToPreventCollision = ( export const addAppNameToPreventCollision = (
composeData: ComposeSpecification, composeData: ComposeSpecification,
appName: string appName: string,
): ComposeSpecification => { ): ComposeSpecification => {
let updatedComposeData = { ...composeData }; let updatedComposeData = { ...composeData };
updatedComposeData = addAppNameToAllServiceNames(updatedComposeData, appName); updatedComposeData = addAppNameToAllServiceNames(updatedComposeData, appName);
updatedComposeData = addSuffixToAllVolumes(updatedComposeData, appName); updatedComposeData = addSuffixToAllVolumes(updatedComposeData, appName);
return updatedComposeData; return updatedComposeData;
}; };
export const randomizeIsolatedDeploymentComposeFile = async ( export const randomizeIsolatedDeploymentComposeFile = async (
composeId: string, composeId: string,
suffix?: string suffix?: string,
) => { ) => {
const compose = await findComposeById(composeId); const compose = await findComposeById(composeId);
const composeFile = compose.composeFile; const composeFile = compose.composeFile;
const composeData = load(composeFile) as ComposeSpecification; const composeData = load(composeFile) as ComposeSpecification;
const randomSuffix = suffix || compose.appName || generateRandomHash(); const randomSuffix = suffix || compose.appName || generateRandomHash();
const newComposeFile = addAppNameToPreventCollision( const newComposeFile = addAppNameToPreventCollision(
composeData, composeData,
randomSuffix randomSuffix,
); );
return dump(newComposeFile); return dump(newComposeFile);
}; };
export const randomizeDeployableSpecificationFile = ( export const randomizeDeployableSpecificationFile = (
composeSpec: ComposeSpecification, composeSpec: ComposeSpecification,
suffix?: string suffix?: string,
) => { ) => {
if (!suffix) { if (!suffix) {
return composeSpec; return composeSpec;
} }
const newComposeFile = addAppNameToPreventCollision(composeSpec, suffix); const newComposeFile = addAppNameToPreventCollision(composeSpec, suffix);
return newComposeFile; return newComposeFile;
}; };

View File

@@ -7,346 +7,346 @@ import type { Domain } from "@dokploy/server/services/domain";
import { dump, load } from "js-yaml"; import { dump, load } from "js-yaml";
import { execAsyncRemote } from "../process/execAsync"; import { execAsyncRemote } from "../process/execAsync";
import { import {
cloneRawBitbucketRepository, cloneRawBitbucketRepository,
cloneRawBitbucketRepositoryRemote, cloneRawBitbucketRepositoryRemote,
} from "../providers/bitbucket"; } from "../providers/bitbucket";
import { import {
cloneGitRawRepository, cloneGitRawRepository,
cloneRawGitRepositoryRemote, cloneRawGitRepositoryRemote,
} from "../providers/git"; } from "../providers/git";
import { import {
cloneRawGithubRepository, cloneRawGithubRepository,
cloneRawGithubRepositoryRemote, cloneRawGithubRepositoryRemote,
} from "../providers/github"; } from "../providers/github";
import { import {
cloneRawGitlabRepository, cloneRawGitlabRepository,
cloneRawGitlabRepositoryRemote, cloneRawGitlabRepositoryRemote,
} from "../providers/gitlab"; } from "../providers/gitlab";
import { import {
createComposeFileRaw, createComposeFileRaw,
createComposeFileRawRemote, createComposeFileRawRemote,
} from "../providers/raw"; } from "../providers/raw";
import { randomizeDeployableSpecificationFile } from "./collision"; import { randomizeDeployableSpecificationFile } from "./collision";
import { randomizeSpecificationFile } from "./compose"; import { randomizeSpecificationFile } from "./compose";
import type { import type {
ComposeSpecification, ComposeSpecification,
DefinitionsService, DefinitionsService,
PropertiesNetworks, PropertiesNetworks,
} from "./types"; } from "./types";
import { encodeBase64 } from "./utils"; import { encodeBase64 } from "./utils";
export const cloneCompose = async (compose: Compose) => { export const cloneCompose = async (compose: Compose) => {
if (compose.sourceType === "github") { if (compose.sourceType === "github") {
await cloneRawGithubRepository(compose); await cloneRawGithubRepository(compose);
} else if (compose.sourceType === "gitlab") { } else if (compose.sourceType === "gitlab") {
await cloneRawGitlabRepository(compose); await cloneRawGitlabRepository(compose);
} else if (compose.sourceType === "bitbucket") { } else if (compose.sourceType === "bitbucket") {
await cloneRawBitbucketRepository(compose); await cloneRawBitbucketRepository(compose);
} else if (compose.sourceType === "git") { } else if (compose.sourceType === "git") {
await cloneGitRawRepository(compose); await cloneGitRawRepository(compose);
} else if (compose.sourceType === "raw") { } else if (compose.sourceType === "raw") {
await createComposeFileRaw(compose); await createComposeFileRaw(compose);
} }
}; };
export const cloneComposeRemote = async (compose: Compose) => { export const cloneComposeRemote = async (compose: Compose) => {
if (compose.sourceType === "github") { if (compose.sourceType === "github") {
await cloneRawGithubRepositoryRemote(compose); await cloneRawGithubRepositoryRemote(compose);
} else if (compose.sourceType === "gitlab") { } else if (compose.sourceType === "gitlab") {
await cloneRawGitlabRepositoryRemote(compose); await cloneRawGitlabRepositoryRemote(compose);
} else if (compose.sourceType === "bitbucket") { } else if (compose.sourceType === "bitbucket") {
await cloneRawBitbucketRepositoryRemote(compose); await cloneRawBitbucketRepositoryRemote(compose);
} else if (compose.sourceType === "git") { } else if (compose.sourceType === "git") {
await cloneRawGitRepositoryRemote(compose); await cloneRawGitRepositoryRemote(compose);
} else if (compose.sourceType === "raw") { } else if (compose.sourceType === "raw") {
await createComposeFileRawRemote(compose); await createComposeFileRawRemote(compose);
} }
}; };
export const getComposePath = (compose: Compose) => { export const getComposePath = (compose: Compose) => {
const { COMPOSE_PATH } = paths(!!compose.serverId); const { COMPOSE_PATH } = paths(!!compose.serverId);
const { appName, sourceType, composePath } = compose; const { appName, sourceType, composePath } = compose;
let path = ""; let path = "";
if (sourceType === "raw") { if (sourceType === "raw") {
path = "docker-compose.yml"; path = "docker-compose.yml";
} else { } else {
path = composePath; path = composePath;
} }
return join(COMPOSE_PATH, appName, "code", path); return join(COMPOSE_PATH, appName, "code", path);
}; };
export const loadDockerCompose = async ( export const loadDockerCompose = async (
compose: Compose compose: Compose,
): Promise<ComposeSpecification | null> => { ): Promise<ComposeSpecification | null> => {
const path = getComposePath(compose); const path = getComposePath(compose);
if (existsSync(path)) { if (existsSync(path)) {
const yamlStr = readFileSync(path, "utf8"); const yamlStr = readFileSync(path, "utf8");
const parsedConfig = load(yamlStr) as ComposeSpecification; const parsedConfig = load(yamlStr) as ComposeSpecification;
return parsedConfig; return parsedConfig;
} }
return null; return null;
}; };
export const loadDockerComposeRemote = async ( export const loadDockerComposeRemote = async (
compose: Compose compose: Compose,
): Promise<ComposeSpecification | null> => { ): Promise<ComposeSpecification | null> => {
const path = getComposePath(compose); const path = getComposePath(compose);
try { try {
if (!compose.serverId) { if (!compose.serverId) {
return null; return null;
} }
const { stdout, stderr } = await execAsyncRemote( const { stdout, stderr } = await execAsyncRemote(
compose.serverId, compose.serverId,
`cat ${path}` `cat ${path}`,
); );
if (stderr) { if (stderr) {
return null; return null;
} }
if (!stdout) return null; if (!stdout) return null;
const parsedConfig = load(stdout) as ComposeSpecification; const parsedConfig = load(stdout) as ComposeSpecification;
return parsedConfig; return parsedConfig;
} catch (err) { } catch (err) {
return null; return null;
} }
}; };
export const readComposeFile = async (compose: Compose) => { export const readComposeFile = async (compose: Compose) => {
const path = getComposePath(compose); const path = getComposePath(compose);
if (existsSync(path)) { if (existsSync(path)) {
const yamlStr = readFileSync(path, "utf8"); const yamlStr = readFileSync(path, "utf8");
return yamlStr; return yamlStr;
} }
return null; return null;
}; };
export const writeDomainsToCompose = async ( export const writeDomainsToCompose = async (
compose: Compose, compose: Compose,
domains: Domain[] domains: Domain[],
) => { ) => {
if (!domains.length) { if (!domains.length) {
return; return;
} }
const composeConverted = await addDomainToCompose(compose, domains); const composeConverted = await addDomainToCompose(compose, domains);
const path = getComposePath(compose); const path = getComposePath(compose);
const composeString = dump(composeConverted, { lineWidth: 1000 }); const composeString = dump(composeConverted, { lineWidth: 1000 });
try { try {
await writeFile(path, composeString, "utf8"); await writeFile(path, composeString, "utf8");
} catch (error) { } catch (error) {
throw error; throw error;
} }
}; };
export const writeDomainsToComposeRemote = async ( export const writeDomainsToComposeRemote = async (
compose: Compose, compose: Compose,
domains: Domain[], domains: Domain[],
logPath: string logPath: string,
) => { ) => {
if (!domains.length) { if (!domains.length) {
return ""; return "";
} }
try { try {
const composeConverted = await addDomainToCompose(compose, domains); const composeConverted = await addDomainToCompose(compose, domains);
const path = getComposePath(compose); const path = getComposePath(compose);
if (!composeConverted) { if (!composeConverted) {
return ` return `
echo "❌ Error: Compose file not found" >> ${logPath}; echo "❌ Error: Compose file not found" >> ${logPath};
exit 1; exit 1;
`; `;
} }
if (compose.serverId) { if (compose.serverId) {
const composeString = dump(composeConverted, { lineWidth: 1000 }); const composeString = dump(composeConverted, { lineWidth: 1000 });
const encodedContent = encodeBase64(composeString); const encodedContent = encodeBase64(composeString);
return `echo "${encodedContent}" | base64 -d > "${path}";`; return `echo "${encodedContent}" | base64 -d > "${path}";`;
} }
} catch (error) { } catch (error) {
// @ts-ignore // @ts-ignore
return `echo "❌ Has occured an error: ${error?.message || error}" >> ${logPath}; return `echo "❌ Has occured an error: ${error?.message || error}" >> ${logPath};
exit 1; exit 1;
`; `;
} }
}; };
// (node:59875) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 SIGTERM listeners added to [process]. Use emitter.setMaxListeners() to increase limit // (node:59875) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 SIGTERM listeners added to [process]. Use emitter.setMaxListeners() to increase limit
export const addDomainToCompose = async ( export const addDomainToCompose = async (
compose: Compose, compose: Compose,
domains: Domain[] domains: Domain[],
) => { ) => {
const { appName } = compose; const { appName } = compose;
let result: ComposeSpecification | null; let result: ComposeSpecification | null;
if (compose.serverId) { if (compose.serverId) {
result = await loadDockerComposeRemote(compose); // aca hay que ir al servidor e ir a traer el compose file al servidor result = await loadDockerComposeRemote(compose); // aca hay que ir al servidor e ir a traer el compose file al servidor
} else { } else {
result = await loadDockerCompose(compose); result = await loadDockerCompose(compose);
} }
if (!result || domains.length === 0) { if (!result || domains.length === 0) {
return null; return null;
} }
if (compose.isolatedDeployment) { if (compose.isolatedDeployment) {
const randomized = randomizeDeployableSpecificationFile( const randomized = randomizeDeployableSpecificationFile(
result, result,
compose.suffix || compose.appName compose.suffix || compose.appName,
); );
result = randomized; result = randomized;
} else if (compose.randomize) { } else if (compose.randomize) {
const randomized = randomizeSpecificationFile(result, compose.suffix); const randomized = randomizeSpecificationFile(result, compose.suffix);
result = randomized; result = randomized;
} }
for (const domain of domains) { for (const domain of domains) {
const { serviceName, https } = domain; const { serviceName, https } = domain;
if (!serviceName) { if (!serviceName) {
throw new Error("Service name not found"); throw new Error("Service name not found");
} }
if (!result?.services?.[serviceName]) { if (!result?.services?.[serviceName]) {
throw new Error(`The service ${serviceName} not found in the compose`); throw new Error(`The service ${serviceName} not found in the compose`);
} }
const httpLabels = await createDomainLabels(appName, domain, "web"); const httpLabels = await createDomainLabels(appName, domain, "web");
if (https) { if (https) {
const httpsLabels = await createDomainLabels( const httpsLabels = await createDomainLabels(
appName, appName,
domain, domain,
"websecure" "websecure",
); );
httpLabels.push(...httpsLabels); httpLabels.push(...httpsLabels);
} }
let labels: DefinitionsService["labels"] = []; let labels: DefinitionsService["labels"] = [];
if (compose.composeType === "docker-compose") { if (compose.composeType === "docker-compose") {
if (!result.services[serviceName].labels) { if (!result.services[serviceName].labels) {
result.services[serviceName].labels = []; result.services[serviceName].labels = [];
} }
labels = result.services[serviceName].labels; labels = result.services[serviceName].labels;
} else { } else {
// Stack Case // Stack Case
if (!result.services[serviceName].deploy) { if (!result.services[serviceName].deploy) {
result.services[serviceName].deploy = {}; result.services[serviceName].deploy = {};
} }
if (!result.services[serviceName].deploy.labels) { if (!result.services[serviceName].deploy.labels) {
result.services[serviceName].deploy.labels = []; result.services[serviceName].deploy.labels = [];
} }
labels = result.services[serviceName].deploy.labels; labels = result.services[serviceName].deploy.labels;
} }
if (Array.isArray(labels)) { if (Array.isArray(labels)) {
if (!labels.includes("traefik.enable=true")) { if (!labels.includes("traefik.enable=true")) {
labels.push("traefik.enable=true"); labels.push("traefik.enable=true");
} }
labels.push(...httpLabels); labels.push(...httpLabels);
} }
if (!compose.isolatedDeployment) { if (!compose.isolatedDeployment) {
// Add the dokploy-network to the service // Add the dokploy-network to the service
result.services[serviceName].networks = addDokployNetworkToService( result.services[serviceName].networks = addDokployNetworkToService(
result.services[serviceName].networks result.services[serviceName].networks,
); );
} }
} }
// Add dokploy-network to the root of the compose file // Add dokploy-network to the root of the compose file
if (!compose.isolatedDeployment) { if (!compose.isolatedDeployment) {
result.networks = addDokployNetworkToRoot(result.networks); result.networks = addDokployNetworkToRoot(result.networks);
} }
return result; return result;
}; };
export const writeComposeFile = async ( export const writeComposeFile = async (
compose: Compose, compose: Compose,
composeSpec: ComposeSpecification composeSpec: ComposeSpecification,
) => { ) => {
const path = getComposePath(compose); const path = getComposePath(compose);
try { try {
const composeFile = dump(composeSpec, { const composeFile = dump(composeSpec, {
lineWidth: 1000, lineWidth: 1000,
}); });
fs.writeFileSync(path, composeFile, "utf8"); fs.writeFileSync(path, composeFile, "utf8");
} catch (e) { } catch (e) {
console.error("Error saving the YAML config file:", e); console.error("Error saving the YAML config file:", e);
} }
}; };
export const createDomainLabels = async ( export const createDomainLabels = async (
appName: string, appName: string,
domain: Domain, domain: Domain,
entrypoint: "web" | "websecure" entrypoint: "web" | "websecure",
) => { ) => {
const { host, port, https, uniqueConfigKey, certificateType, path } = domain; const { host, port, https, uniqueConfigKey, certificateType, path } = domain;
const routerName = `${appName}-${uniqueConfigKey}-${entrypoint}`; const routerName = `${appName}-${uniqueConfigKey}-${entrypoint}`;
const labels = [ const labels = [
`traefik.http.routers.${routerName}.rule=Host(\`${host}\`)${path && path !== "/" ? ` && PathPrefix(\`${path}\`)` : ""}`, `traefik.http.routers.${routerName}.rule=Host(\`${host}\`)${path && path !== "/" ? ` && PathPrefix(\`${path}\`)` : ""}`,
`traefik.http.routers.${routerName}.entrypoints=${entrypoint}`, `traefik.http.routers.${routerName}.entrypoints=${entrypoint}`,
`traefik.http.services.${routerName}.loadbalancer.server.port=${port}`, `traefik.http.services.${routerName}.loadbalancer.server.port=${port}`,
`traefik.http.routers.${routerName}.service=${routerName}`, `traefik.http.routers.${routerName}.service=${routerName}`,
]; ];
if (entrypoint === "web" && https) { if (entrypoint === "web" && https) {
labels.push( labels.push(
`traefik.http.routers.${routerName}.middlewares=redirect-to-https@file` `traefik.http.routers.${routerName}.middlewares=redirect-to-https@file`,
); );
} }
if (entrypoint === "websecure") { if (entrypoint === "websecure") {
if (certificateType === "letsencrypt") { if (certificateType === "letsencrypt") {
labels.push( labels.push(
`traefik.http.routers.${routerName}.tls.certresolver=letsencrypt` `traefik.http.routers.${routerName}.tls.certresolver=letsencrypt`,
); );
} }
} }
return labels; return labels;
}; };
export const addDokployNetworkToService = ( export const addDokployNetworkToService = (
networkService: DefinitionsService["networks"] networkService: DefinitionsService["networks"],
) => { ) => {
let networks = networkService; let networks = networkService;
const network = "dokploy-network"; const network = "dokploy-network";
if (!networks) { if (!networks) {
networks = []; networks = [];
} }
if (Array.isArray(networks)) { if (Array.isArray(networks)) {
if (!networks.includes(network)) { if (!networks.includes(network)) {
networks.push(network); networks.push(network);
} }
} else if (networks && typeof networks === "object") { } else if (networks && typeof networks === "object") {
if (!(network in networks)) { if (!(network in networks)) {
networks[network] = {}; networks[network] = {};
} }
} }
return networks; return networks;
}; };
export const addDokployNetworkToRoot = ( export const addDokployNetworkToRoot = (
networkRoot: PropertiesNetworks | undefined networkRoot: PropertiesNetworks | undefined,
) => { ) => {
let networks = networkRoot; let networks = networkRoot;
const network = "dokploy-network"; const network = "dokploy-network";
if (!networks) { if (!networks) {
networks = {}; networks = {};
} }
if (networks[network] || !networks[network]) { if (networks[network] || !networks[network]) {
networks[network] = { networks[network] = {
external: true, external: true,
}; };
} }
return networks; return networks;
}; };