Merge branch 'canary' into 187-backups-for-docker-compose

This commit is contained in:
Mauricio Siu
2025-05-03 09:48:24 -06:00
37 changed files with 2133 additions and 169 deletions

View File

@@ -6,6 +6,7 @@ import {
type apiCreateDeployment,
type apiCreateDeploymentCompose,
type apiCreateDeploymentPreview,
type apiCreateDeploymentSchedule,
type apiCreateDeploymentServer,
deployments,
} from "@dokploy/server/db/schema";
@@ -27,6 +28,7 @@ import {
findPreviewDeploymentById,
updatePreviewDeployment,
} from "./preview-deployment";
import { findScheduleById } from "./schedule";
export type Deployment = typeof deployments.$inferSelect;
@@ -57,6 +59,7 @@ export const createDeployment = async (
try {
await removeLastTenDeployments(
deployment.applicationId,
"application",
application.serverId,
);
const { LOGS_PATH } = paths(!!application.serverId);
@@ -88,6 +91,7 @@ export const createDeployment = async (
status: "running",
logPath: logFilePath,
description: deployment.description || "",
startedAt: new Date().toISOString(),
})
.returning();
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
@@ -107,6 +111,8 @@ export const createDeployment = async (
logPath: "",
description: deployment.description || "",
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
startedAt: new Date().toISOString(),
finishedAt: new Date().toISOString(),
})
.returning();
await updateApplicationStatus(application.applicationId, "error");
@@ -128,8 +134,9 @@ export const createDeploymentPreview = async (
deployment.previewDeploymentId,
);
try {
await removeLastTenPreviewDeploymenById(
await removeLastTenDeployments(
deployment.previewDeploymentId,
"previewDeployment",
previewDeployment?.application?.serverId,
);
@@ -165,6 +172,7 @@ export const createDeploymentPreview = async (
logPath: logFilePath,
description: deployment.description || "",
previewDeploymentId: deployment.previewDeploymentId,
startedAt: new Date().toISOString(),
})
.returning();
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
@@ -184,6 +192,8 @@ export const createDeploymentPreview = async (
logPath: "",
description: deployment.description || "",
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
startedAt: new Date().toISOString(),
finishedAt: new Date().toISOString(),
})
.returning();
await updatePreviewDeployment(deployment.previewDeploymentId, {
@@ -205,8 +215,9 @@ export const createDeploymentCompose = async (
) => {
const compose = await findComposeById(deployment.composeId);
try {
await removeLastTenComposeDeployments(
await removeLastTenDeployments(
deployment.composeId,
"compose",
compose.serverId,
);
const { LOGS_PATH } = paths(!!compose.serverId);
@@ -238,6 +249,7 @@ echo "Initializing deployment" >> ${logFilePath};
description: deployment.description || "",
status: "running",
logPath: logFilePath,
startedAt: new Date().toISOString(),
})
.returning();
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
@@ -257,6 +269,8 @@ echo "Initializing deployment" >> ${logFilePath};
logPath: "",
description: deployment.description || "",
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
startedAt: new Date().toISOString(),
finishedAt: new Date().toISOString(),
})
.returning();
await updateCompose(compose.composeId, {
@@ -270,6 +284,82 @@ echo "Initializing deployment" >> ${logFilePath};
}
};
export const createDeploymentSchedule = async (
deployment: Omit<
typeof apiCreateDeploymentSchedule._type,
"deploymentId" | "createdAt" | "status" | "logPath"
>,
) => {
const schedule = await findScheduleById(deployment.scheduleId);
try {
const serverId =
schedule.application?.serverId ||
schedule.compose?.serverId ||
schedule.server?.serverId;
await removeLastTenDeployments(deployment.scheduleId, "schedule", serverId);
const { SCHEDULES_PATH } = paths(!!serverId);
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
const fileName = `${schedule.appName}-${formattedDateTime}.log`;
const logFilePath = path.join(SCHEDULES_PATH, schedule.appName, fileName);
if (serverId) {
const server = await findServerById(serverId);
const command = `
mkdir -p ${SCHEDULES_PATH}/${schedule.appName};
echo "Initializing schedule" >> ${logFilePath};
`;
await execAsyncRemote(server.serverId, command);
} else {
await fsPromises.mkdir(path.join(SCHEDULES_PATH, schedule.appName), {
recursive: true,
});
await fsPromises.writeFile(logFilePath, "Initializing schedule\n");
}
const deploymentCreate = await db
.insert(deployments)
.values({
scheduleId: deployment.scheduleId,
title: deployment.title || "Deployment",
status: "running",
logPath: logFilePath,
description: deployment.description || "",
startedAt: new Date().toISOString(),
})
.returning();
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the deployment",
});
}
return deploymentCreate[0];
} catch (error) {
console.log(error);
await db
.insert(deployments)
.values({
scheduleId: deployment.scheduleId,
title: deployment.title || "Deployment",
status: "error",
logPath: "",
description: deployment.description || "",
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
startedAt: new Date().toISOString(),
finishedAt: new Date().toISOString(),
})
.returning();
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the deployment",
});
}
};
export const removeDeployment = async (deploymentId: string) => {
try {
const deployment = await db
@@ -296,109 +386,15 @@ export const removeDeploymentsByApplicationId = async (
.returning();
};
const removeLastTenDeployments = async (
applicationId: string,
serverId: string | null,
const getDeploymentsByType = async (
id: string,
type: "application" | "compose" | "server" | "schedule" | "previewDeployment",
) => {
const deploymentList = await db.query.deployments.findMany({
where: eq(deployments.applicationId, applicationId),
where: eq(deployments[`${type}Id`], id),
orderBy: desc(deployments.createdAt),
});
if (deploymentList.length > 10) {
const deploymentsToDelete = deploymentList.slice(9);
if (serverId) {
let command = "";
for (const oldDeployment of deploymentsToDelete) {
const logPath = path.join(oldDeployment.logPath);
command += `
rm -rf ${logPath};
`;
await removeDeployment(oldDeployment.deploymentId);
}
await execAsyncRemote(serverId, command);
} else {
for (const oldDeployment of deploymentsToDelete) {
const logPath = path.join(oldDeployment.logPath);
if (existsSync(logPath)) {
await fsPromises.unlink(logPath);
}
await removeDeployment(oldDeployment.deploymentId);
}
}
}
};
const removeLastTenComposeDeployments = async (
composeId: string,
serverId: string | null,
) => {
const deploymentList = await db.query.deployments.findMany({
where: eq(deployments.composeId, composeId),
orderBy: desc(deployments.createdAt),
});
if (deploymentList.length > 10) {
if (serverId) {
let command = "";
const deploymentsToDelete = deploymentList.slice(9);
for (const oldDeployment of deploymentsToDelete) {
const logPath = path.join(oldDeployment.logPath);
command += `
rm -rf ${logPath};
`;
await removeDeployment(oldDeployment.deploymentId);
}
await execAsyncRemote(serverId, command);
} else {
const deploymentsToDelete = deploymentList.slice(9);
for (const oldDeployment of deploymentsToDelete) {
const logPath = path.join(oldDeployment.logPath);
if (existsSync(logPath)) {
await fsPromises.unlink(logPath);
}
await removeDeployment(oldDeployment.deploymentId);
}
}
}
};
export const removeLastTenPreviewDeploymenById = async (
previewDeploymentId: string,
serverId: string | null,
) => {
const deploymentList = await db.query.deployments.findMany({
where: eq(deployments.previewDeploymentId, previewDeploymentId),
orderBy: desc(deployments.createdAt),
});
if (deploymentList.length > 10) {
const deploymentsToDelete = deploymentList.slice(9);
if (serverId) {
let command = "";
for (const oldDeployment of deploymentsToDelete) {
const logPath = path.join(oldDeployment.logPath);
command += `
rm -rf ${logPath};
`;
await removeDeployment(oldDeployment.deploymentId);
}
await execAsyncRemote(serverId, command);
} else {
for (const oldDeployment of deploymentsToDelete) {
const logPath = path.join(oldDeployment.logPath);
if (existsSync(logPath)) {
await fsPromises.unlink(logPath);
}
await removeDeployment(oldDeployment.deploymentId);
}
}
}
return deploymentList;
};
export const removeDeployments = async (application: Application) => {
@@ -413,6 +409,38 @@ export const removeDeployments = async (application: Application) => {
await removeDeploymentsByApplicationId(applicationId);
};
const removeLastTenDeployments = async (
id: string,
type: "application" | "compose" | "server" | "schedule" | "previewDeployment",
serverId?: string | null,
) => {
const deploymentList = await getDeploymentsByType(id, type);
if (deploymentList.length > 10) {
const deploymentsToDelete = deploymentList.slice(10);
if (serverId) {
let command = "";
for (const oldDeployment of deploymentsToDelete) {
const logPath = path.join(oldDeployment.logPath);
command += `
rm -rf ${logPath};
`;
await removeDeployment(oldDeployment.deploymentId);
}
await execAsyncRemote(serverId, command);
} else {
for (const oldDeployment of deploymentsToDelete) {
const logPath = path.join(oldDeployment.logPath);
if (existsSync(logPath)) {
await fsPromises.unlink(logPath);
}
await removeDeployment(oldDeployment.deploymentId);
}
}
}
};
export const removeDeploymentsByPreviewDeploymentId = async (
previewDeployment: PreviewDeployment,
serverId: string | null,
@@ -494,6 +522,10 @@ export const updateDeploymentStatus = async (
.update(deployments)
.set({
status: deploymentStatus,
finishedAt:
deploymentStatus === "done" || deploymentStatus === "error"
? new Date().toISOString()
: null,
})
.where(eq(deployments.deploymentId, deploymentId))
.returning();

View File

@@ -0,0 +1,126 @@
import { type Schedule, schedules } from "../db/schema/schedule";
import { db } from "../db";
import { eq } from "drizzle-orm";
import { TRPCError } from "@trpc/server";
import type { z } from "zod";
import type {
createScheduleSchema,
updateScheduleSchema,
} from "../db/schema/schedule";
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
import { paths } from "../constants";
import path from "node:path";
import { encodeBase64 } from "../utils/docker/utils";
export type ScheduleExtended = Awaited<ReturnType<typeof findScheduleById>>;
export const createSchedule = async (
input: z.infer<typeof createScheduleSchema>,
) => {
const { scheduleId, ...rest } = input;
const [newSchedule] = await db.insert(schedules).values(rest).returning();
if (
newSchedule &&
(newSchedule.scheduleType === "dokploy-server" ||
newSchedule.scheduleType === "server")
) {
await handleScript(newSchedule);
}
return newSchedule;
};
export const findScheduleById = async (scheduleId: string) => {
const schedule = await db.query.schedules.findFirst({
where: eq(schedules.scheduleId, scheduleId),
with: {
application: true,
compose: true,
server: true,
},
});
if (!schedule) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Schedule not found",
});
}
return schedule;
};
export const deleteSchedule = async (scheduleId: string) => {
const schedule = await findScheduleById(scheduleId);
const serverId =
schedule?.serverId ||
schedule?.application?.serverId ||
schedule?.compose?.serverId;
const { SCHEDULES_PATH } = paths(!!serverId);
const fullPath = path.join(SCHEDULES_PATH, schedule?.appName || "");
const command = `rm -rf ${fullPath}`;
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
const scheduleResult = await db
.delete(schedules)
.where(eq(schedules.scheduleId, scheduleId));
if (!scheduleResult) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Schedule not found",
});
}
return true;
};
export const updateSchedule = async (
input: z.infer<typeof updateScheduleSchema>,
) => {
const { scheduleId, ...rest } = input;
const [updatedSchedule] = await db
.update(schedules)
.set(rest)
.where(eq(schedules.scheduleId, scheduleId))
.returning();
if (!updatedSchedule) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Schedule not found",
});
}
if (
updatedSchedule?.scheduleType === "dokploy-server" ||
updatedSchedule?.scheduleType === "server"
) {
await handleScript(updatedSchedule);
}
return updatedSchedule;
};
const handleScript = async (schedule: Schedule) => {
const { SCHEDULES_PATH } = paths(!!schedule?.serverId);
const fullPath = path.join(SCHEDULES_PATH, schedule?.appName || "");
const encodedContent = encodeBase64(schedule?.script || "");
const script = `
mkdir -p ${fullPath}
rm -f ${fullPath}/script.sh
touch ${fullPath}/script.sh
chmod +x ${fullPath}/script.sh
echo "${encodedContent}" | base64 -d > ${fullPath}/script.sh
`;
if (schedule?.scheduleType === "dokploy-server") {
await execAsync(script);
} else if (schedule?.scheduleType === "server") {
await execAsyncRemote(schedule?.serverId || "", script);
}
};