Merge branch 'canary' into kucherenko/canary

This commit is contained in:
Mauricio Siu
2025-01-18 18:29:12 -06:00
335 changed files with 20594 additions and 17450 deletions

View File

@@ -1,4 +1,3 @@
import { webcrypto } from "node:crypto";
import type { IncomingMessage, ServerResponse } from "node:http";
import { findAdminByAuthId } from "@dokploy/server/services/admin";
import { findUserByAuthId } from "@dokploy/server/services/user";
@@ -9,7 +8,6 @@ import type { Session, User } from "lucia/dist/core.js";
import { db } from "../db";
import { type DatabaseUser, auth, sessionTable } from "../db/schema";
globalThis.crypto = webcrypto as Crypto;
export const adapter = new DrizzlePostgreSQLAdapter(db, sessionTable, auth);
export const lucia = new Lucia(adapter, {

View File

@@ -129,10 +129,10 @@ export const applications = pgTable("application", {
false,
),
buildArgs: text("buildArgs"),
memoryReservation: integer("memoryReservation"),
memoryLimit: integer("memoryLimit"),
cpuReservation: integer("cpuReservation"),
cpuLimit: integer("cpuLimit"),
memoryReservation: text("memoryReservation"),
memoryLimit: text("memoryLimit"),
cpuReservation: text("cpuReservation"),
cpuLimit: text("cpuLimit"),
title: text("title"),
enabled: boolean("enabled"),
subtitle: text("subtitle"),
@@ -355,10 +355,10 @@ const createSchema = createInsertSchema(applications, {
buildArgs: z.string().optional(),
name: z.string().min(1),
description: z.string().optional(),
memoryReservation: z.number().optional(),
memoryLimit: z.number().optional(),
cpuReservation: z.number().optional(),
cpuLimit: z.number().optional(),
memoryReservation: z.string().optional(),
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
title: z.string().optional(),
enabled: z.boolean().optional(),
subtitle: z.string().optional(),

View File

@@ -29,10 +29,10 @@ export const mariadb = pgTable("mariadb", {
command: text("command"),
env: text("env"),
// RESOURCES
memoryReservation: integer("memoryReservation"),
memoryLimit: integer("memoryLimit"),
cpuReservation: integer("cpuReservation"),
cpuLimit: integer("cpuLimit"),
memoryReservation: text("memoryReservation"),
memoryLimit: text("memoryLimit"),
cpuReservation: text("cpuReservation"),
cpuLimit: text("cpuLimit"),
//
externalPort: integer("externalPort"),
applicationStatus: applicationStatus("applicationStatus")
@@ -74,10 +74,10 @@ const createSchema = createInsertSchema(mariadb, {
dockerImage: z.string().default("mariadb:6"),
command: z.string().optional(),
env: z.string().optional(),
memoryReservation: z.number().optional(),
memoryLimit: z.number().optional(),
cpuReservation: z.number().optional(),
cpuLimit: z.number().optional(),
memoryReservation: z.string().optional(),
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
projectId: z.string(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
externalPort: z.number(),

View File

@@ -26,10 +26,10 @@ export const mongo = pgTable("mongo", {
dockerImage: text("dockerImage").notNull(),
command: text("command"),
env: text("env"),
memoryReservation: integer("memoryReservation"),
memoryLimit: integer("memoryLimit"),
cpuReservation: integer("cpuReservation"),
cpuLimit: integer("cpuLimit"),
memoryReservation: text("memoryReservation"),
memoryLimit: text("memoryLimit"),
cpuReservation: text("cpuReservation"),
cpuLimit: text("cpuLimit"),
externalPort: integer("externalPort"),
applicationStatus: applicationStatus("applicationStatus")
.notNull()
@@ -69,10 +69,10 @@ const createSchema = createInsertSchema(mongo, {
dockerImage: z.string().default("mongo:15"),
command: z.string().optional(),
env: z.string().optional(),
memoryReservation: z.number().optional(),
memoryLimit: z.number().optional(),
cpuReservation: z.number().optional(),
cpuLimit: z.number().optional(),
memoryReservation: z.string().optional(),
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
projectId: z.string(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
externalPort: z.number(),

View File

@@ -28,10 +28,10 @@ export const mysql = pgTable("mysql", {
dockerImage: text("dockerImage").notNull(),
command: text("command"),
env: text("env"),
memoryReservation: integer("memoryReservation"),
memoryLimit: integer("memoryLimit"),
cpuReservation: integer("cpuReservation"),
cpuLimit: integer("cpuLimit"),
memoryReservation: text("memoryReservation"),
memoryLimit: text("memoryLimit"),
cpuReservation: text("cpuReservation"),
cpuLimit: text("cpuLimit"),
externalPort: integer("externalPort"),
applicationStatus: applicationStatus("applicationStatus")
.notNull()
@@ -72,10 +72,10 @@ const createSchema = createInsertSchema(mysql, {
dockerImage: z.string().default("mysql:8"),
command: z.string().optional(),
env: z.string().optional(),
memoryReservation: z.number().optional(),
memoryLimit: z.number().optional(),
cpuReservation: z.number().optional(),
cpuLimit: z.number().optional(),
memoryReservation: z.string().optional(),
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
projectId: z.string(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
externalPort: z.number(),

View File

@@ -10,6 +10,7 @@ export const notificationType = pgEnum("notificationType", [
"telegram",
"discord",
"email",
"gotify",
]);
export const notifications = pgTable("notification", {
@@ -39,6 +40,9 @@ export const notifications = pgTable("notification", {
emailId: text("emailId").references(() => email.emailId, {
onDelete: "cascade",
}),
gotifyId: text("gotifyId").references(() => gotify.gotifyId, {
onDelete: "cascade",
}),
adminId: text("adminId").references(() => admins.adminId, {
onDelete: "cascade",
}),
@@ -84,6 +88,17 @@ export const email = pgTable("email", {
toAddresses: text("toAddress").array().notNull(),
});
export const gotify = pgTable("gotify", {
gotifyId: text("gotifyId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
serverUrl: text("serverUrl").notNull(),
appToken: text("appToken").notNull(),
priority: integer("priority").notNull().default(5),
decoration: boolean("decoration"),
});
export const notificationsRelations = relations(notifications, ({ one }) => ({
slack: one(slack, {
fields: [notifications.slackId],
@@ -101,6 +116,10 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
fields: [notifications.emailId],
references: [email.emailId],
}),
gotify: one(gotify, {
fields: [notifications.gotifyId],
references: [gotify.gotifyId],
}),
admin: one(admins, {
fields: [notifications.adminId],
references: [admins.adminId],
@@ -224,6 +243,39 @@ export const apiTestEmailConnection = apiCreateEmail.pick({
fromAddress: true,
});
export const apiCreateGotify = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployRestart: true,
name: true,
appDeploy: true,
dockerCleanup: true,
})
.extend({
serverUrl: z.string().min(1),
appToken: z.string().min(1),
priority: z.number().min(1),
decoration: z.boolean(),
})
.required();
export const apiUpdateGotify = apiCreateGotify.partial().extend({
notificationId: z.string().min(1),
gotifyId: z.string().min(1),
adminId: z.string().optional(),
});
export const apiTestGotifyConnection = apiCreateGotify
.pick({
serverUrl: true,
appToken: true,
priority: true,
})
.extend({
decoration: z.boolean().optional(),
});
export const apiFindOneNotification = notificationsSchema
.pick({
notificationId: true,
@@ -242,5 +294,8 @@ export const apiSendTest = notificationsSchema
username: z.string(),
password: z.string(),
toAddresses: z.array(z.string()),
serverUrl: z.string(),
appToken: z.string(),
priority: z.number(),
})
.partial();

View File

@@ -27,11 +27,11 @@ export const postgres = pgTable("postgres", {
dockerImage: text("dockerImage").notNull(),
command: text("command"),
env: text("env"),
memoryReservation: integer("memoryReservation"),
memoryReservation: text("memoryReservation"),
externalPort: integer("externalPort"),
memoryLimit: integer("memoryLimit"),
cpuReservation: integer("cpuReservation"),
cpuLimit: integer("cpuLimit"),
memoryLimit: text("memoryLimit"),
cpuReservation: text("cpuReservation"),
cpuLimit: text("cpuLimit"),
applicationStatus: applicationStatus("applicationStatus")
.notNull()
.default("idle"),
@@ -68,10 +68,10 @@ const createSchema = createInsertSchema(postgres, {
dockerImage: z.string().default("postgres:15"),
command: z.string().optional(),
env: z.string().optional(),
memoryReservation: z.number().optional(),
memoryLimit: z.number().optional(),
cpuReservation: z.number().optional(),
cpuLimit: z.number().optional(),
memoryReservation: z.string().optional(),
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
projectId: z.string(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
externalPort: z.number(),

View File

@@ -24,10 +24,10 @@ export const redis = pgTable("redis", {
dockerImage: text("dockerImage").notNull(),
command: text("command"),
env: text("env"),
memoryReservation: integer("memoryReservation"),
memoryLimit: integer("memoryLimit"),
cpuReservation: integer("cpuReservation"),
cpuLimit: integer("cpuLimit"),
memoryReservation: text("memoryReservation"),
memoryLimit: text("memoryLimit"),
cpuReservation: text("cpuReservation"),
cpuLimit: text("cpuLimit"),
externalPort: integer("externalPort"),
createdAt: text("createdAt")
.notNull()
@@ -64,10 +64,10 @@ const createSchema = createInsertSchema(redis, {
dockerImage: z.string().default("redis:8"),
command: z.string().optional(),
env: z.string().optional(),
memoryReservation: z.number().optional(),
memoryLimit: z.number().optional(),
cpuReservation: z.number().optional(),
cpuLimit: z.number().optional(),
memoryReservation: z.string().optional(),
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
projectId: z.string(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
externalPort: z.number(),

View File

@@ -67,7 +67,7 @@ export const apiCreateRegistry = createSchema
});
export const apiTestRegistry = createSchema.pick({}).extend({
registryName: z.string().min(1),
registryName: z.string().optional(),
username: z.string().min(1),
password: z.string().min(1),
registryUrl: z.string(),

View File

@@ -40,11 +40,11 @@ export const users = pgTable("user", {
canAccessToTraefikFiles: boolean("canAccessToTraefikFiles")
.notNull()
.default(false),
accesedProjects: text("accesedProjects")
accessedProjects: text("accesedProjects")
.array()
.notNull()
.default(sql`ARRAY[]::text[]`),
accesedServices: text("accesedServices")
accessedServices: text("accesedServices")
.array()
.notNull()
.default(sql`ARRAY[]::text[]`),
@@ -73,8 +73,8 @@ const createSchema = createInsertSchema(users, {
token: z.string().min(1),
isRegistered: z.boolean().optional(),
adminId: z.string(),
accesedProjects: z.array(z.string()).optional(),
accesedServices: z.array(z.string()).optional(),
accessedProjects: z.array(z.string()).optional(),
accessedServices: z.array(z.string()).optional(),
canCreateProjects: z.boolean().optional(),
canCreateServices: z.boolean().optional(),
canDeleteProjects: z.boolean().optional(),
@@ -106,8 +106,8 @@ export const apiAssignPermissions = createSchema
canCreateServices: true,
canDeleteProjects: true,
canDeleteServices: true,
accesedProjects: true,
accesedServices: true,
accessedProjects: true,
accessedServices: true,
canAccessToTraefikFiles: true,
canAccessToDocker: true,
canAccessToAPI: true,

View File

@@ -213,6 +213,7 @@ export const deployApplication = async ({
applicationType: "application",
buildLink,
adminId: application.project.adminId,
domains: application.domains
});
} catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error");
@@ -332,6 +333,7 @@ export const deployRemoteApplication = async ({
applicationType: "application",
buildLink,
adminId: application.project.adminId,
domains: application.domains
});
} catch (error) {
// @ts-ignore

View File

@@ -243,6 +243,7 @@ export const deployCompose = async ({
applicationType: "compose",
buildLink,
adminId: compose.project.adminId,
domains: compose.domains,
});
} catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error");
@@ -366,6 +367,7 @@ export const deployRemoteCompose = async ({
applicationType: "compose",
buildLink,
adminId: compose.project.adminId,
domains: compose.domains,
});
} catch (error) {
// @ts-ignore

View File

@@ -120,23 +120,33 @@ export const findMariadbByBackupId = async (backupId: string) => {
return result[0];
};
export const deployMariadb = async (mariadbId: string) => {
export const deployMariadb = async (
mariadbId: string,
onData?: (data: any) => void,
) => {
const mariadb = await findMariadbById(mariadbId);
try {
await updateMariadbById(mariadbId, {
applicationStatus: "running",
});
onData?.("Starting mariadb deployment...");
if (mariadb.serverId) {
await execAsyncRemote(
mariadb.serverId,
`docker pull ${mariadb.dockerImage}`,
onData,
);
} else {
await pullImage(mariadb.dockerImage);
await pullImage(mariadb.dockerImage, onData);
}
await buildMariadb(mariadb);
await updateMariadbById(mariadbId, {
applicationStatus: "done",
});
onData?.("Deployment completed successfully!");
} catch (error) {
onData?.(`Error: ${error}`);
await updateMariadbById(mariadbId, {
applicationStatus: "error",
});

View File

@@ -112,20 +112,34 @@ export const removeMongoById = async (mongoId: string) => {
return result[0];
};
export const deployMongo = async (mongoId: string) => {
export const deployMongo = async (
mongoId: string,
onData?: (data: any) => void,
) => {
const mongo = await findMongoById(mongoId);
try {
await updateMongoById(mongoId, {
applicationStatus: "running",
});
onData?.("Starting mongo deployment...");
if (mongo.serverId) {
await execAsyncRemote(mongo.serverId, `docker pull ${mongo.dockerImage}`);
await execAsyncRemote(
mongo.serverId,
`docker pull ${mongo.dockerImage}`,
onData,
);
} else {
await pullImage(mongo.dockerImage);
await pullImage(mongo.dockerImage, onData);
}
await buildMongo(mongo);
await updateMongoById(mongoId, {
applicationStatus: "done",
});
onData?.("Deployment completed successfully!");
} catch (error) {
onData?.(`Error: ${error}`);
await updateMongoById(mongoId, {
applicationStatus: "error",
});

View File

@@ -1,6 +1,6 @@
import { db } from "@dokploy/server/db";
import { type apiCreateMySql, backups, mysql } from "@dokploy/server/db/schema";
import { buildAppName, cleanAppName } from "@dokploy/server/db/schema";
import { buildAppName } from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates/utils";
import { buildMysql } from "@dokploy/server/utils/databases/mysql";
import { pullImage } from "@dokploy/server/utils/docker/utils";
@@ -116,20 +116,33 @@ export const removeMySqlById = async (mysqlId: string) => {
return result[0];
};
export const deployMySql = async (mysqlId: string) => {
export const deployMySql = async (
mysqlId: string,
onData?: (data: any) => void,
) => {
const mysql = await findMySqlById(mysqlId);
try {
await updateMySqlById(mysqlId, {
applicationStatus: "running",
});
onData?.("Starting mysql deployment...");
if (mysql.serverId) {
await execAsyncRemote(mysql.serverId, `docker pull ${mysql.dockerImage}`);
await execAsyncRemote(
mysql.serverId,
`docker pull ${mysql.dockerImage}`,
onData,
);
} else {
await pullImage(mysql.dockerImage);
await pullImage(mysql.dockerImage, onData);
}
await buildMysql(mysql);
await updateMySqlById(mysqlId, {
applicationStatus: "done",
});
onData?.("Deployment completed successfully!");
} catch (error) {
onData?.(`Error: ${error}`);
await updateMySqlById(mysqlId, {
applicationStatus: "error",
});

View File

@@ -2,14 +2,17 @@ import { db } from "@dokploy/server/db";
import {
type apiCreateDiscord,
type apiCreateEmail,
type apiCreateGotify,
type apiCreateSlack,
type apiCreateTelegram,
type apiUpdateDiscord,
type apiUpdateEmail,
type apiUpdateGotify,
type apiUpdateSlack,
type apiUpdateTelegram,
discord,
email,
gotify,
notifications,
slack,
telegram,
@@ -379,6 +382,96 @@ export const updateEmailNotification = async (
});
};
export const createGotifyNotification = async (
input: typeof apiCreateGotify._type,
adminId: string,
) => {
await db.transaction(async (tx) => {
const newGotify = await tx
.insert(gotify)
.values({
serverUrl: input.serverUrl,
appToken: input.appToken,
priority: input.priority,
decoration: input.decoration,
})
.returning()
.then((value) => value[0]);
if (!newGotify) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting gotify",
});
}
const newDestination = await tx
.insert(notifications)
.values({
gotifyId: newGotify.gotifyId,
name: input.name,
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "gotify",
adminId: adminId,
})
.returning()
.then((value) => value[0]);
if (!newDestination) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting notification",
});
}
return newDestination;
});
};
export const updateGotifyNotification = async (
input: typeof apiUpdateGotify._type,
) => {
await db.transaction(async (tx) => {
const newDestination = await tx
.update(notifications)
.set({
name: input.name,
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
adminId: input.adminId,
})
.where(eq(notifications.notificationId, input.notificationId))
.returning()
.then((value) => value[0]);
if (!newDestination) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error Updating notification",
});
}
await tx
.update(gotify)
.set({
serverUrl: input.serverUrl,
appToken: input.appToken,
priority: input.priority,
decoration: input.decoration,
})
.where(eq(gotify.gotifyId, input.gotifyId));
return newDestination;
});
};
export const findNotificationById = async (notificationId: string) => {
const notification = await db.query.notifications.findFirst({
where: eq(notifications.notificationId, notificationId),
@@ -387,6 +480,7 @@ export const findNotificationById = async (notificationId: string) => {
telegram: true,
discord: true,
email: true,
gotify: true,
},
});
if (!notification) {

View File

@@ -115,24 +115,37 @@ export const removePostgresById = async (postgresId: string) => {
return result[0];
};
export const deployPostgres = async (postgresId: string) => {
export const deployPostgres = async (
postgresId: string,
onData?: (data: any) => void,
) => {
const postgres = await findPostgresById(postgresId);
try {
const promises = [];
await updatePostgresById(postgresId, {
applicationStatus: "running",
});
onData?.("Starting postgres deployment...");
if (postgres.serverId) {
const result = await execAsyncRemote(
await execAsyncRemote(
postgres.serverId,
`docker pull ${postgres.dockerImage}`,
onData,
);
} else {
await pullImage(postgres.dockerImage);
await pullImage(postgres.dockerImage, onData);
}
await buildPostgres(postgres);
await updatePostgresById(postgresId, {
applicationStatus: "done",
});
onData?.("Deployment completed successfully!");
} catch (error) {
onData?.(`Error: ${error}`);
await updatePostgresById(postgresId, {
applicationStatus: "error",
});

View File

@@ -89,20 +89,34 @@ export const removeRedisById = async (redisId: string) => {
return result[0];
};
export const deployRedis = async (redisId: string) => {
export const deployRedis = async (
redisId: string,
onData?: (data: any) => void,
) => {
const redis = await findRedisById(redisId);
try {
await updateRedisById(redisId, {
applicationStatus: "running",
});
onData?.("Starting redis deployment...");
if (redis.serverId) {
await execAsyncRemote(redis.serverId, `docker pull ${redis.dockerImage}`);
await execAsyncRemote(
redis.serverId,
`docker pull ${redis.dockerImage}`,
onData,
);
} else {
await pullImage(redis.dockerImage);
await pullImage(redis.dockerImage, onData);
}
await buildRedis(redis);
await updateRedisById(redisId, {
applicationStatus: "done",
});
onData?.("Deployment completed successfully!");
} catch (error) {
onData?.(`Error: ${error}`);
await updateRedisById(redisId, {
applicationStatus: "error",
});

View File

@@ -54,7 +54,7 @@ export const addNewProject = async (authId: string, projectId: string) => {
await db
.update(users)
.set({
accesedProjects: [...user.accesedProjects, projectId],
accessedProjects: [...user.accessedProjects, projectId],
})
.where(eq(users.authId, authId));
};
@@ -64,7 +64,7 @@ export const addNewService = async (authId: string, serviceId: string) => {
await db
.update(users)
.set({
accesedServices: [...user.accesedServices, serviceId],
accessedServices: [...user.accessedServices, serviceId],
})
.where(eq(users.authId, authId));
};
@@ -73,8 +73,8 @@ export const canPerformCreationService = async (
userId: string,
projectId: string,
) => {
const { accesedProjects, canCreateServices } = await findUserByAuthId(userId);
const haveAccessToProject = accesedProjects.includes(projectId);
const { accessedProjects, canCreateServices } = await findUserByAuthId(userId);
const haveAccessToProject = accessedProjects.includes(projectId);
if (canCreateServices && haveAccessToProject) {
return true;
@@ -87,8 +87,8 @@ export const canPerformAccessService = async (
userId: string,
serviceId: string,
) => {
const { accesedServices } = await findUserByAuthId(userId);
const haveAccessToService = accesedServices.includes(serviceId);
const { accessedServices } = await findUserByAuthId(userId);
const haveAccessToService = accessedServices.includes(serviceId);
if (haveAccessToService) {
return true;
@@ -101,8 +101,8 @@ export const canPeformDeleteService = async (
authId: string,
serviceId: string,
) => {
const { accesedServices, canDeleteServices } = await findUserByAuthId(authId);
const haveAccessToService = accesedServices.includes(serviceId);
const { accessedServices, canDeleteServices } = await findUserByAuthId(authId);
const haveAccessToService = accessedServices.includes(serviceId);
if (canDeleteServices && haveAccessToService) {
return true;
@@ -135,9 +135,9 @@ export const canPerformAccessProject = async (
authId: string,
projectId: string,
) => {
const { accesedProjects } = await findUserByAuthId(authId);
const { accessedProjects } = await findUserByAuthId(authId);
const haveAccessToProject = accesedProjects.includes(projectId);
const haveAccessToProject = accessedProjects.includes(projectId);
if (haveAccessToProject) {
return true;

View File

@@ -1,4 +1,3 @@
import { createWriteStream } from "node:fs";
import path from "node:path";
import { paths } from "@dokploy/server/constants";
import {
@@ -32,7 +31,10 @@ export const slugify = (text: string | undefined) => {
});
};
export const serverSetup = async (serverId: string) => {
export const serverSetup = async (
serverId: string,
onData?: (data: any) => void,
) => {
const server = await findServerById(serverId);
const { LOGS_PATH } = paths();
@@ -48,18 +50,18 @@ export const serverSetup = async (serverId: string) => {
description: "Setup Server",
});
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
try {
writeStream.write("\nInstalling Server Dependencies: ✅\n");
await installRequirements(serverId, deployment.logPath);
writeStream.close();
onData?.("\nInstalling Server Dependencies: ✅\n");
await installRequirements(serverId, onData);
await updateDeploymentStatus(deployment.deploymentId, "done");
onData?.("\nSetup Server: ✅\n");
} catch (err) {
console.log(err);
await updateDeploymentStatus(deployment.deploymentId, "error");
writeStream.write(`${err}\n`);
writeStream.close();
onData?.(`${err}\n`);
}
};
@@ -173,13 +175,14 @@ ${installBuildpacks()}
return bashCommand;
};
const installRequirements = async (serverId: string, logPath: string) => {
const writeStream = createWriteStream(logPath, { flags: "a" });
const installRequirements = async (
serverId: string,
onData?: (data: any) => void,
) => {
const client = new Client();
const server = await findServerById(serverId);
if (!server.sshKeyId) {
writeStream.write("❌ No SSH Key found");
writeStream.close();
onData?.("❌ No SSH Key found, please assign one to this server");
throw new Error("No SSH Key found");
}
@@ -189,7 +192,7 @@ const installRequirements = async (serverId: string, logPath: string) => {
const command = server.command || defaultCommand();
client.exec(command, (err, stream) => {
if (err) {
writeStream.write(err);
onData?.(err.message);
reject(err);
return;
}
@@ -199,17 +202,17 @@ const installRequirements = async (serverId: string, logPath: string) => {
resolve();
})
.on("data", (data: string) => {
writeStream.write(data.toString());
onData?.(data.toString());
})
.stderr.on("data", (data) => {
writeStream.write(data.toString());
onData?.(data.toString());
});
});
})
.on("error", (err) => {
client.end();
if (err.level === "client-authentication") {
writeStream.write(
onData?.(
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
);
reject(
@@ -218,9 +221,7 @@ const installRequirements = async (serverId: string, logPath: string) => {
),
);
} else {
writeStream.write(
`SSH connection error: ${err.message} ${err.level}`,
);
onData?.(`SSH connection error: ${err.message} ${err.level}`);
reject(new Error(`SSH connection error: ${err.message}`));
}
})

View File

@@ -2,7 +2,6 @@ import { createWriteStream } from "node:fs";
import { join } from "node:path";
import type { InferResultType } from "@dokploy/server/types/with";
import type { CreateServiceOptions } from "dockerode";
import { nanoid } from "nanoid";
import { uploadImage, uploadImageRemoteCommand } from "../cluster/upload";
import {
calculateResources,

View File

@@ -306,10 +306,10 @@ export const generateVolumeMounts = (mounts: ApplicationNested["mounts"]) => {
};
type Resources = {
memoryLimit: number | null;
memoryReservation: number | null;
cpuLimit: number | null;
cpuReservation: number | null;
memoryLimit: string | null;
memoryReservation: string | null;
cpuLimit: string | null;
cpuReservation: string | null;
};
export const calculateResources = ({
memoryLimit,
@@ -319,12 +319,14 @@ export const calculateResources = ({
}: Resources): ResourceRequirements => {
return {
Limits: {
MemoryBytes: memoryLimit ?? undefined,
NanoCPUs: cpuLimit ?? undefined,
MemoryBytes: memoryLimit ? Number.parseInt(memoryLimit) : undefined,
NanoCPUs: cpuLimit ? Number.parseInt(cpuLimit) : undefined,
},
Reservations: {
MemoryBytes: memoryReservation ?? undefined,
NanoCPUs: cpuReservation ?? undefined,
MemoryBytes: memoryReservation
? Number.parseInt(memoryReservation)
: undefined,
NanoCPUs: cpuReservation ? Number.parseInt(cpuReservation) : undefined,
},
};
};

View File

@@ -3,9 +3,11 @@ import { notifications } from "@dokploy/server/db/schema";
import BuildFailedEmail from "@dokploy/server/emails/emails/build-failed";
import { renderAsync } from "@react-email/components";
import { and, eq } from "drizzle-orm";
import { format } from "date-fns";
import {
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -39,11 +41,12 @@ export const sendBuildErrorNotifications = async ({
discord: true,
telegram: true,
slack: true,
gotify: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack } = notification;
const { email, discord, telegram, slack, gotify } = notification;
if (email) {
const template = await renderAsync(
BuildFailedEmail({
@@ -112,22 +115,35 @@ export const sendBuildErrorNotifications = async ({
});
}
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate("⚠️", "Build Failed"),
`${decorate("🛠️", `Project: ${projectName}`)}` +
`${decorate("⚙️", `Application: ${applicationName}`)}` +
`${decorate("❔", `Type: ${applicationType}`)}` +
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${decorate("⚠️", `Error:\n${errorMessage}`)}` +
`${decorate("🔗", `Build details:\n${buildLink}`)}`,
);
}
if (telegram) {
const inlineButton = [
[
{
text: "Deployment Logs",
url: buildLink,
},
],
];
await sendTelegramNotification(
telegram,
`
<b>⚠️ Build Failed</b>
<b>Project:</b> ${projectName}
<b>Application:</b> ${applicationName}
<b>Type:</b> ${applicationType}
<b>Time:</b> ${date.toLocaleString()}
<b>Error:</b>
<pre>${errorMessage}</pre>
<b>Build Details:</b> ${buildLink}
`,
`<b>⚠️ Build Failed</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`,
inlineButton
);
}

View File

@@ -1,11 +1,14 @@
import { db } from "@dokploy/server/db";
import { notifications } from "@dokploy/server/db/schema";
import BuildSuccessEmail from "@dokploy/server/emails/emails/build-success";
import { Domain } from "@dokploy/server/services/domain";
import { renderAsync } from "@react-email/components";
import { and, eq } from "drizzle-orm";
import { format } from "date-fns";
import {
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -16,6 +19,7 @@ interface Props {
applicationType: string;
buildLink: string;
adminId: string;
domains: Domain[];
}
export const sendBuildSuccessNotifications = async ({
@@ -24,6 +28,7 @@ export const sendBuildSuccessNotifications = async ({
applicationType,
buildLink,
adminId,
domains
}: Props) => {
const date = new Date();
const unixDate = ~~(Number(date) / 1000);
@@ -37,11 +42,12 @@ export const sendBuildSuccessNotifications = async ({
discord: true,
telegram: true,
slack: true,
gotify: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack } = notification;
const { email, discord, telegram, slack, gotify } = notification;
if (email) {
const template = await renderAsync(
@@ -61,7 +67,7 @@ export const sendBuildSuccessNotifications = async ({
`${discord.decoration ? decoration : ""} ${text}`.trim();
await sendDiscordNotification(discord, {
title: "> `✅` Build Success",
title: decorate(">", "`✅` Build Success"),
color: 0x57f287,
fields: [
{
@@ -106,19 +112,44 @@ export const sendBuildSuccessNotifications = async ({
});
}
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate("✅", "Build Success"),
`${decorate("🛠️", `Project: ${projectName}`)}` +
`${decorate("⚙️", `Application: ${applicationName}`)}` +
`${decorate("❔", `Type: ${applicationType}`)}` +
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${decorate("🔗", `Build details:\n${buildLink}`)}`,
);
}
if (telegram) {
const chunkArray = <T>(array: T[], chunkSize: number): T[][] =>
Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) => array.slice(i * chunkSize, i * chunkSize + chunkSize)
);
const inlineButton = [
[
{
text: "Deployment Logs",
url: buildLink,
},
],
...chunkArray(domains, 2).map((chunk) =>
chunk.map((data) => ({
text: data.host,
url: `${data.https ? "https" : "http"}://${data.host}`,
}))
),
];
await sendTelegramNotification(
telegram,
`
<b>✅ Build Success</b>
<b>Project:</b> ${projectName}
<b>Application:</b> ${applicationName}
<b>Type:</b> ${applicationType}
<b>Time:</b> ${date.toLocaleString()}
<b>Build Details:</b> ${buildLink}
`,
`<b>✅ Build Success</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
inlineButton
);
}

View File

@@ -1,11 +1,14 @@
import { error } from "node:console";
import { db } from "@dokploy/server/db";
import { notifications } from "@dokploy/server/db/schema";
import DatabaseBackupEmail from "@dokploy/server/emails/emails/database-backup";
import { renderAsync } from "@react-email/components";
import { and, eq } from "drizzle-orm";
import { format } from "date-fns";
import {
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -37,11 +40,12 @@ export const sendDatabaseBackupNotifications = async ({
discord: true,
telegram: true,
slack: true,
gotify: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack } = notification;
const { email, discord, telegram, slack, gotify } = notification;
if (email) {
const template = await renderAsync(
@@ -120,19 +124,33 @@ export const sendDatabaseBackupNotifications = async ({
});
}
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate(
type === "success" ? "✅" : "❌",
`Database Backup ${type === "success" ? "Successful" : "Failed"}`,
),
`${decorate("🛠️", `Project: ${projectName}`)}` +
`${decorate("⚙️", `Application: ${applicationName}`)}` +
`${decorate("❔", `Type: ${databaseType}`)}` +
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`,
);
}
if (telegram) {
const isError = type === "error" && errorMessage;
const statusEmoji = type === "success" ? "✅" : "❌";
const messageText = `
<b>${statusEmoji} Database Backup ${type === "success" ? "Successful" : "Failed"}</b>
<b>Project:</b> ${projectName}
<b>Application:</b> ${applicationName}
<b>Type:</b> ${databaseType}
<b>Time:</b> ${date.toLocaleString()}
<b>Status:</b> ${type === "success" ? "Successful" : "Failed"}
${type === "error" && errorMessage ? `<b>Error:</b> ${errorMessage}` : ""}
`;
const typeStatus = type === "success" ? "Successful" : "Failed";
const errorMsg = isError ? `\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>` : "";
const messageText = `<b>${statusEmoji} Database Backup ${typeStatus}</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${databaseType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;
await sendTelegramNotification(telegram, messageText);
}

View File

@@ -3,9 +3,11 @@ import { notifications } from "@dokploy/server/db/schema";
import DockerCleanupEmail from "@dokploy/server/emails/emails/docker-cleanup";
import { renderAsync } from "@react-email/components";
import { and, eq } from "drizzle-orm";
import { format } from "date-fns";
import {
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -26,11 +28,12 @@ export const sendDockerCleanupNotifications = async (
discord: true,
telegram: true,
slack: true,
gotify: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack } = notification;
const { email, discord, telegram, slack, gotify } = notification;
if (email) {
const template = await renderAsync(
@@ -79,14 +82,21 @@ export const sendDockerCleanupNotifications = async (
});
}
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate("✅", "Docker Cleanup"),
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${decorate("📜", `Message:\n${message}`)}`,
);
}
if (telegram) {
await sendTelegramNotification(
telegram,
`
<b>✅ Docker Cleanup</b>
<b>Message:</b> ${message}
<b>Time:</b> ${date.toLocaleString()}
`,
`<b>✅ Docker Cleanup</b>\n\n<b>Message:</b> ${message}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`
);
}

View File

@@ -6,9 +6,11 @@ import { eq } from "drizzle-orm";
import {
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
import { format } from "date-fns";
export const sendDokployRestartNotifications = async () => {
const date = new Date();
@@ -20,11 +22,12 @@ export const sendDokployRestartNotifications = async () => {
discord: true,
telegram: true,
slack: true,
gotify: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack } = notification;
const { email, discord, telegram, slack, gotify } = notification;
if (email) {
const template = await renderAsync(
@@ -64,13 +67,20 @@ export const sendDokployRestartNotifications = async () => {
});
}
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate("✅", "Dokploy Server Restarted"),
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}`,
);
}
if (telegram) {
await sendTelegramNotification(
telegram,
`
<b>✅ Dokploy Serverd Restarted</b>
<b>Time:</b> ${date.toLocaleString()}
`,
`<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`
);
}

View File

@@ -1,6 +1,7 @@
import type {
discord,
email,
gotify,
slack,
telegram,
} from "@dokploy/server/db/schema";
@@ -41,20 +42,24 @@ export const sendDiscordNotification = async (
connection: typeof discord.$inferInsert,
embed: any,
) => {
try {
await fetch(connection.webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ embeds: [embed] }),
});
} catch (err) {
console.log(err);
}
// try {
await fetch(connection.webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ embeds: [embed] }),
});
// } catch (err) {
// console.log(err);
// }
};
export const sendTelegramNotification = async (
connection: typeof telegram.$inferInsert,
messageText: string,
inlineButton?: {
text: string;
url: string;
}[][]
) => {
try {
const url = `https://api.telegram.org/bot${connection.botToken}/sendMessage`;
@@ -66,6 +71,9 @@ export const sendTelegramNotification = async (
text: messageText,
parse_mode: "HTML",
disable_web_page_preview: true,
reply_markup: {
inline_keyboard: inlineButton,
},
}),
});
} catch (err) {
@@ -87,3 +95,33 @@ export const sendSlackNotification = async (
console.log(err);
}
};
export const sendGotifyNotification = async (
connection: typeof gotify.$inferInsert,
title: string,
message: string,
) => {
const response = await fetch(`${connection.serverUrl}/message`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Gotify-Key": connection.appToken,
},
body: JSON.stringify({
title: title,
message: message,
priority: connection.priority,
extras: {
"client::display": {
contentType: "text/plain",
},
},
}),
});
if (!response.ok) {
throw new Error(
`Failed to send Gotify notification: ${response.statusText}`,
);
}
};

View File

@@ -7,6 +7,7 @@ export const execAsync = util.promisify(exec);
export const execAsyncRemote = async (
serverId: string | null,
command: string,
onData?: (data: string) => void,
): Promise<{ stdout: string; stderr: string }> => {
if (!serverId) return { stdout: "", stderr: "" };
const server = await findServerById(serverId);
@@ -21,7 +22,10 @@ export const execAsyncRemote = async (
conn
.once("ready", () => {
conn.exec(command, (err, stream) => {
if (err) throw err;
if (err) {
onData?.(err.message);
throw err;
}
stream
.on("close", (code: number, signal: string) => {
conn.end();
@@ -37,21 +41,27 @@ export const execAsyncRemote = async (
})
.on("data", (data: string) => {
stdout += data.toString();
onData?.(data.toString());
})
.stderr.on("data", (data) => {
stderr += data.toString();
onData?.(data.toString());
});
});
})
.on("error", (err) => {
conn.end();
if (err.level === "client-authentication") {
onData?.(
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
);
reject(
new Error(
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
),
);
} else {
onData?.(`SSH connection error: ${err.message}`);
reject(new Error(`SSH connection error: ${err.message}`));
}
})