mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge branch 'canary' into feat/stack-env-support
This commit is contained in:
@@ -111,7 +111,7 @@ class LogRotationManager {
|
||||
);
|
||||
console.log("USR1 Signal send to Traefik");
|
||||
} catch (error) {
|
||||
console.error("Error to send USR1 Signal to Traefik:", error);
|
||||
console.error("Error sending USR1 Signal to Traefik:", error);
|
||||
}
|
||||
}
|
||||
public async getStatus(): Promise<boolean> {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
cleanUpSystemPrune,
|
||||
cleanUpUnusedImages,
|
||||
} from "../docker/utils";
|
||||
import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup";
|
||||
import { runMariadbBackup } from "./mariadb";
|
||||
import { runMongoBackup } from "./mongo";
|
||||
import { runMySqlBackup } from "./mysql";
|
||||
@@ -25,21 +26,26 @@ export const initCronJobs = async () => {
|
||||
await cleanUpUnusedImages();
|
||||
await cleanUpDockerBuilder();
|
||||
await cleanUpSystemPrune();
|
||||
await sendDockerCleanupNotifications(admin.adminId);
|
||||
});
|
||||
}
|
||||
|
||||
const servers = await getAllServers();
|
||||
|
||||
for (const server of servers) {
|
||||
const { appName, serverId } = server;
|
||||
if (serverId) {
|
||||
const { serverId, enableDockerCleanup, name } = server;
|
||||
if (enableDockerCleanup) {
|
||||
scheduleJob(serverId, "0 0 * * *", async () => {
|
||||
console.log(
|
||||
`SERVER-BACKUP[${new Date().toLocaleString()}] Running Cleanup ${appName}`,
|
||||
`SERVER-BACKUP[${new Date().toLocaleString()}] Running Cleanup ${name}`,
|
||||
);
|
||||
await cleanUpUnusedImages(serverId);
|
||||
await cleanUpDockerBuilder(serverId);
|
||||
await cleanUpSystemPrune(serverId);
|
||||
await sendDockerCleanupNotifications(
|
||||
admin.adminId,
|
||||
`Docker cleanup for Server ${name} (${serverId})`,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -59,8 +65,11 @@ export const initCronJobs = async () => {
|
||||
});
|
||||
for (const pg of pgs) {
|
||||
for (const backup of pg.backups) {
|
||||
const { schedule, backupId, enabled } = backup;
|
||||
const { schedule, backupId, enabled, database } = backup;
|
||||
if (enabled) {
|
||||
console.log(
|
||||
`[Backup] Postgres DB ${pg.name} for ${database} Activated`,
|
||||
);
|
||||
scheduleJob(backupId, schedule, async () => {
|
||||
console.log(
|
||||
`PG-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
|
||||
@@ -87,8 +96,11 @@ export const initCronJobs = async () => {
|
||||
|
||||
for (const maria of mariadbs) {
|
||||
for (const backup of maria.backups) {
|
||||
const { schedule, backupId, enabled } = backup;
|
||||
const { schedule, backupId, enabled, database } = backup;
|
||||
if (enabled) {
|
||||
console.log(
|
||||
`[Backup] MariaDB DB ${maria.name} for ${database} Activated`,
|
||||
);
|
||||
scheduleJob(backupId, schedule, async () => {
|
||||
console.log(
|
||||
`MARIADB-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
|
||||
@@ -117,6 +129,7 @@ export const initCronJobs = async () => {
|
||||
for (const backup of mongo.backups) {
|
||||
const { schedule, backupId, enabled } = backup;
|
||||
if (enabled) {
|
||||
console.log(`[Backup] MongoDB DB ${mongo.name} Activated`);
|
||||
scheduleJob(backupId, schedule, async () => {
|
||||
console.log(
|
||||
`MONGO-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
|
||||
@@ -145,6 +158,7 @@ export const initCronJobs = async () => {
|
||||
for (const backup of mysql.backups) {
|
||||
const { schedule, backupId, enabled } = backup;
|
||||
if (enabled) {
|
||||
console.log(`[Backup] MySQL DB ${mysql.name} Activated`);
|
||||
scheduleJob(backupId, schedule, async () => {
|
||||
console.log(
|
||||
`MYSQL-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
|
||||
|
||||
@@ -49,6 +49,7 @@ Compose Type: ${composeType} ✅`;
|
||||
writeStream.write(`\n${logBox}\n`);
|
||||
|
||||
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
|
||||
|
||||
await spawnAsync(
|
||||
"docker",
|
||||
[...command.split(" ")],
|
||||
@@ -68,7 +69,7 @@ Compose Type: ${composeType} ✅`;
|
||||
|
||||
writeStream.write("Docker Compose Deployed: ✅");
|
||||
} catch (error) {
|
||||
writeStream.write("Error ❌");
|
||||
writeStream.write(`Error ❌ ${(error as Error).message}`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
@@ -148,6 +149,10 @@ const sanitizeCommand = (command: string) => {
|
||||
export const createCommand = (compose: ComposeNested) => {
|
||||
const { composeType, appName, sourceType } = compose;
|
||||
|
||||
if (compose.command) {
|
||||
return `${sanitizeCommand(compose.command)}`;
|
||||
}
|
||||
|
||||
const path =
|
||||
sourceType === "raw"
|
||||
? composeType === "stack"
|
||||
@@ -161,7 +166,6 @@ export const createCommand = (compose: ComposeNested) => {
|
||||
composeType === "docker-compose"
|
||||
? `compose -p ${appName} -f ${path} up -d --build --remove-orphans`
|
||||
: `stack deploy -c ${path} ${appName} --prune`;
|
||||
|
||||
const customCommand = sanitizeCommand(compose.command);
|
||||
return customCommand ? `${baseCommand} ${customCommand}` : baseCommand;
|
||||
};
|
||||
|
||||
@@ -27,7 +27,9 @@ export const unzipDrop = async (zipFile: File, application: Application) => {
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
const zip = new AdmZip(buffer);
|
||||
const zipEntries = zip.getEntries();
|
||||
const zipEntries = zip
|
||||
.getEntries()
|
||||
.filter((entry) => !entry.entryName.startsWith("__MACOSX"));
|
||||
|
||||
const rootEntries = zipEntries.filter(
|
||||
(entry) =>
|
||||
@@ -59,14 +61,22 @@ export const unzipDrop = async (zipFile: File, application: Application) => {
|
||||
|
||||
if (!filePath) continue;
|
||||
|
||||
const fullPath = path.join(outputPath, filePath);
|
||||
const fullPath = path.join(outputPath, filePath).replace(/\\/g, "/");
|
||||
|
||||
if (application.serverId) {
|
||||
if (entry.isDirectory) {
|
||||
await execAsyncRemote(application.serverId, `mkdir -p ${fullPath}`);
|
||||
} else {
|
||||
if (!entry.isDirectory) {
|
||||
if (sftp === null) throw new Error("No SFTP connection available");
|
||||
await uploadFileToServer(sftp, entry.getData(), fullPath);
|
||||
try {
|
||||
const dirPath = path.dirname(fullPath);
|
||||
await execAsyncRemote(
|
||||
application.serverId,
|
||||
`mkdir -p "${dirPath}"`,
|
||||
);
|
||||
await uploadFileToServer(sftp, entry.getData(), fullPath);
|
||||
} catch (err) {
|
||||
console.error(`Error uploading file ${fullPath}:`, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (entry.isDirectory) {
|
||||
@@ -103,7 +113,6 @@ const getSFTPConnection = async (serverId: string): Promise<SFTPWrapper> => {
|
||||
port: server.port,
|
||||
username: server.username,
|
||||
privateKey: server.sshKey?.privateKey,
|
||||
timeout: 99999,
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -115,7 +124,10 @@ const uploadFileToServer = (
|
||||
): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
sftp.writeFile(remotePath, data, (err) => {
|
||||
if (err) return reject(err);
|
||||
if (err) {
|
||||
console.error(`SFTP write error for ${remotePath}:`, err);
|
||||
return reject(err);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,7 +17,6 @@ import { buildHeroku, getHerokuCommand } from "./heroku";
|
||||
import { buildNixpacks, getNixpacksCommand } from "./nixpacks";
|
||||
import { buildPaketo, getPaketoCommand } from "./paketo";
|
||||
import { buildStatic, getStaticCommand } from "./static";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
// NIXPACKS codeDirectory = where is the path of the code directory
|
||||
// HEROKU codeDirectory = where is the path of the code directory
|
||||
@@ -211,21 +210,21 @@ const getImageName = (application: ApplicationNested) => {
|
||||
}
|
||||
|
||||
if (registry) {
|
||||
return join(registry.imagePrefix || "", appName);
|
||||
return join(registry.registryUrl, registry.imagePrefix || "", appName);
|
||||
}
|
||||
|
||||
return `${appName}:latest`;
|
||||
};
|
||||
|
||||
const getAuthConfig = (application: ApplicationNested) => {
|
||||
const { registry, username, password, sourceType } = application;
|
||||
const { registry, username, password, sourceType, registryUrl } = application;
|
||||
|
||||
if (sourceType === "docker") {
|
||||
if (username && password) {
|
||||
return {
|
||||
password,
|
||||
username,
|
||||
serveraddress: "https://index.docker.io/v1/",
|
||||
serveraddress: registryUrl || "",
|
||||
};
|
||||
}
|
||||
} else if (registry) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { WriteStream } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import path, { join } from "node:path";
|
||||
import type { ApplicationNested } from "../builders";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
@@ -13,27 +13,32 @@ export const uploadImage = async (
|
||||
throw new Error("Registry not found");
|
||||
}
|
||||
|
||||
const { registryUrl, imagePrefix, registryType } = registry;
|
||||
const { registryUrl, imagePrefix } = registry;
|
||||
const { appName } = application;
|
||||
const imageName = `${appName}:latest`;
|
||||
|
||||
const finalURL = registryUrl;
|
||||
|
||||
const registryTag = join(imagePrefix || "", imageName);
|
||||
const registryTag = path
|
||||
.join(registryUrl, join(imagePrefix || "", imageName))
|
||||
.replace(/\/+/g, "/");
|
||||
|
||||
try {
|
||||
writeStream.write(
|
||||
`📦 [Enabled Registry] Uploading image to ${registry.registryType} | ${registryTag} | ${finalURL}\n`,
|
||||
`📦 [Enabled Registry] Uploading image to ${registry.registryType} | ${imageName} | ${finalURL}\n`,
|
||||
);
|
||||
await spawnAsync(
|
||||
const loginCommand = spawnAsync(
|
||||
"docker",
|
||||
["login", finalURL, "-u", registry.username, "-p", registry.password],
|
||||
["login", finalURL, "-u", registry.username, "--password-stdin"],
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
},
|
||||
);
|
||||
loginCommand.child?.stdin?.write(registry.password);
|
||||
loginCommand.child?.stdin?.end();
|
||||
await loginCommand;
|
||||
|
||||
await spawnAsync("docker", ["tag", imageName, registryTag], (data) => {
|
||||
if (writeStream.writable) {
|
||||
@@ -68,22 +73,23 @@ export const uploadImageRemoteCommand = (
|
||||
|
||||
const finalURL = registryUrl;
|
||||
|
||||
const registryTag = join(imagePrefix || "", imageName);
|
||||
const registryTag = path
|
||||
.join(registryUrl, join(imagePrefix || "", imageName))
|
||||
.replace(/\/+/g, "/");
|
||||
|
||||
try {
|
||||
const command = `
|
||||
echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" >> ${logPath};
|
||||
docker login ${finalURL} -u ${registry.username} -p ${registry.password} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "${registry.password}" | docker login ${finalURL} -u ${registry.username} --password-stdin >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ DockerHub Failed" >> ${logPath};
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ DockerHub Login Success" >> ${logPath};
|
||||
echo "✅ Registry Login Success" >> ${logPath};
|
||||
docker tag ${imageName} ${registryTag} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ Error tagging image" >> ${logPath};
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Image Tagged" >> ${logPath};
|
||||
|
||||
echo "✅ Image Tagged" >> ${logPath};
|
||||
docker push ${registryTag} 2>> ${logPath} || {
|
||||
echo "❌ Error pushing image" >> ${logPath};
|
||||
exit 1;
|
||||
@@ -92,7 +98,6 @@ export const uploadImageRemoteCommand = (
|
||||
`;
|
||||
return command;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -28,17 +28,66 @@ export const buildMongo = async (mongo: MongoNested) => {
|
||||
databasePassword,
|
||||
command,
|
||||
mounts,
|
||||
replicaSets,
|
||||
} = mongo;
|
||||
|
||||
const defaultMongoEnv = `MONGO_INITDB_ROOT_USERNAME=${databaseUser}\nMONGO_INITDB_ROOT_PASSWORD=${databasePassword}${
|
||||
const startupScript = `
|
||||
#!/bin/bash
|
||||
${
|
||||
replicaSets
|
||||
? `
|
||||
mongod --port 27017 --replSet rs0 --bind_ip_all &
|
||||
MONGOD_PID=$!
|
||||
|
||||
# Wait for MongoDB to be ready
|
||||
while ! mongosh --eval "db.adminCommand('ping')" > /dev/null 2>&1; do
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Check if replica set is already initialized
|
||||
REPLICA_STATUS=$(mongosh --quiet --eval "rs.status().ok || 0")
|
||||
|
||||
if [ "$REPLICA_STATUS" != "1" ]; then
|
||||
echo "Initializing replica set..."
|
||||
mongosh --eval '
|
||||
rs.initiate({
|
||||
_id: "rs0",
|
||||
members: [{ _id: 0, host: "localhost:27017", priority: 1 }]
|
||||
});
|
||||
|
||||
// Wait for the replica set to initialize
|
||||
while (!rs.isMaster().ismaster) {
|
||||
sleep(1000);
|
||||
}
|
||||
|
||||
// Create root user after replica set is initialized and we are primary
|
||||
db.getSiblingDB("admin").createUser({
|
||||
user: "${databaseUser}",
|
||||
pwd: "${databasePassword}",
|
||||
roles: ["root"]
|
||||
});
|
||||
'
|
||||
|
||||
else
|
||||
echo "Replica set already initialized."
|
||||
fi
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
${command ?? "wait $MONGOD_PID"}`;
|
||||
|
||||
const defaultMongoEnv = `MONGO_INITDB_ROOT_USERNAME=${databaseUser}\nMONGO_INITDB_ROOT_PASSWORD=${databasePassword}${replicaSets ? "\nMONGO_INITDB_DATABASE=admin" : ""}${
|
||||
env ? `\n${env}` : ""
|
||||
}`;
|
||||
|
||||
const resources = calculateResources({
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
});
|
||||
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
defaultMongoEnv,
|
||||
mongo.project.env,
|
||||
@@ -56,12 +105,17 @@ export const buildMongo = async (mongo: MongoNested) => {
|
||||
Image: dockerImage,
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||
...(command
|
||||
...(replicaSets
|
||||
? {
|
||||
Command: ["/bin/sh"],
|
||||
Args: ["-c", command],
|
||||
Command: ["/bin/bash"],
|
||||
Args: ["-c", startupScript],
|
||||
}
|
||||
: {}),
|
||||
: {
|
||||
...(command && {
|
||||
Command: ["/bin/bash"],
|
||||
Args: ["-c", command],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
Networks: [{ Target: "dokploy-network" }],
|
||||
Resources: {
|
||||
@@ -90,6 +144,7 @@ export const buildMongo = async (mongo: MongoNested) => {
|
||||
: [],
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const service = docker.getService(appName);
|
||||
const inspect = await service.inspect();
|
||||
|
||||
@@ -144,10 +144,11 @@ export const getContainerByName = (name: string): Promise<ContainerInfo> => {
|
||||
};
|
||||
export const cleanUpUnusedImages = async (serverId?: string) => {
|
||||
try {
|
||||
const command = "docker image prune --force";
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, "docker image prune --all --force");
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync("docker image prune --all --force");
|
||||
await execAsync(command);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -157,10 +158,11 @@ export const cleanUpUnusedImages = async (serverId?: string) => {
|
||||
|
||||
export const cleanStoppedContainers = async (serverId?: string) => {
|
||||
try {
|
||||
const command = "docker container prune --force";
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, "docker container prune --force");
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync("docker container prune --force");
|
||||
await execAsync(command);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -170,10 +172,11 @@ export const cleanStoppedContainers = async (serverId?: string) => {
|
||||
|
||||
export const cleanUpUnusedVolumes = async (serverId?: string) => {
|
||||
try {
|
||||
const command = "docker volume prune --force";
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, "docker volume prune --all --force");
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync("docker volume prune --all --force");
|
||||
await execAsync(command);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -199,21 +202,20 @@ export const cleanUpInactiveContainers = async () => {
|
||||
};
|
||||
|
||||
export const cleanUpDockerBuilder = async (serverId?: string) => {
|
||||
const command = "docker builder prune --all --force";
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, "docker builder prune --all --force");
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync("docker builder prune --all --force");
|
||||
await execAsync(command);
|
||||
}
|
||||
};
|
||||
|
||||
export const cleanUpSystemPrune = async (serverId?: string) => {
|
||||
const command = "docker system prune --all --force --volumes";
|
||||
if (serverId) {
|
||||
await execAsyncRemote(
|
||||
serverId,
|
||||
"docker system prune --all --force --volumes",
|
||||
);
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync("docker system prune --all --force --volumes");
|
||||
await execAsync(command);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -238,9 +240,11 @@ export const startServiceRemote = async (serverId: string, appName: string) => {
|
||||
export const removeService = async (
|
||||
appName: string,
|
||||
serverId?: string | null,
|
||||
deleteVolumes = false,
|
||||
) => {
|
||||
try {
|
||||
const command = `docker service rm ${appName}`;
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
@@ -304,10 +308,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,
|
||||
@@ -317,16 +321,14 @@ export const calculateResources = ({
|
||||
}: Resources): ResourceRequirements => {
|
||||
return {
|
||||
Limits: {
|
||||
MemoryBytes: memoryLimit ? memoryLimit * 1024 * 1024 : undefined,
|
||||
NanoCPUs: memoryLimit ? (cpuLimit || 1) * 1000 * 1000 * 1000 : undefined,
|
||||
MemoryBytes: memoryLimit ? Number.parseInt(memoryLimit) : undefined,
|
||||
NanoCPUs: cpuLimit ? Number.parseInt(cpuLimit) : undefined,
|
||||
},
|
||||
Reservations: {
|
||||
MemoryBytes: memoryLimit
|
||||
? (memoryReservation || 1) * 1024 * 1024
|
||||
: undefined,
|
||||
NanoCPUs: memoryLimit
|
||||
? (cpuReservation || 1) * 1000 * 1000 * 1000
|
||||
MemoryBytes: memoryReservation
|
||||
? Number.parseInt(memoryReservation)
|
||||
: undefined,
|
||||
NanoCPUs: cpuReservation ? Number.parseInt(cpuReservation) : undefined,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@ export const removeFileOrDirectory = async (path: string) => {
|
||||
try {
|
||||
await execAsync(`rm -rf ${path}`);
|
||||
} catch (error) {
|
||||
console.error(`Error to remove ${path}: ${error}`);
|
||||
console.error(`Error removing ${path}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -58,7 +58,7 @@ export const removeDirectoryCode = async (
|
||||
await execAsync(command);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error to remove ${directoryPath}: ${error}`);
|
||||
console.error(`Error removing ${directoryPath}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -77,7 +77,7 @@ export const removeComposeDirectory = async (
|
||||
await execAsync(command);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error to remove ${directoryPath}: ${error}`);
|
||||
console.error(`Error removing ${directoryPath}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -96,7 +96,7 @@ export const removeMonitoringDirectory = async (
|
||||
await execAsync(command);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error to remove ${directoryPath}: ${error}`);
|
||||
console.error(`Error removing ${directoryPath}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -35,7 +35,6 @@ export async function checkGPUStatus(serverId?: string): Promise<GPUInfo> {
|
||||
...cudaInfo,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error in checkGPUStatus:", error);
|
||||
return {
|
||||
driverInstalled: false,
|
||||
driverVersion: undefined,
|
||||
@@ -303,7 +302,7 @@ const setupLocalServer = async (daemonConfig: any) => {
|
||||
await fs.writeFile(configFile, JSON.stringify(daemonConfig, null, 2));
|
||||
|
||||
const setupCommands = [
|
||||
`pkexec sh -c '
|
||||
`sudo sh -c '
|
||||
cp ${configFile} /etc/docker/daemon.json &&
|
||||
mkdir -p /etc/nvidia-container-runtime &&
|
||||
sed -i "/swarm-resource/d" /etc/nvidia-container-runtime/config.toml &&
|
||||
@@ -314,7 +313,13 @@ const setupLocalServer = async (daemonConfig: any) => {
|
||||
`rm ${configFile}`,
|
||||
].join(" && ");
|
||||
|
||||
await execAsync(setupCommands);
|
||||
try {
|
||||
await execAsync(setupCommands);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
"Failed to configure GPU support. Please ensure you have sudo privileges and try again.",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const addGpuLabel = async (nodeId: string, serverId?: string) => {
|
||||
@@ -337,11 +342,10 @@ const verifySetup = async (nodeId: string, serverId?: string) => {
|
||||
"cat /etc/nvidia-container-runtime/config.toml",
|
||||
].join(" && ");
|
||||
|
||||
const { stdout: diagnostics } = serverId
|
||||
? await execAsyncRemote(serverId, diagnosticCommands)
|
||||
: await execAsync(diagnosticCommands);
|
||||
await (serverId
|
||||
? execAsyncRemote(serverId, diagnosticCommands)
|
||||
: execAsync(diagnosticCommands));
|
||||
|
||||
console.error("Diagnostic Information:", diagnostics);
|
||||
throw new Error("GPU support not detected in swarm after setup");
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@ import { db } from "@dokploy/server/db";
|
||||
import { notifications } from "@dokploy/server/db/schema";
|
||||
import BuildFailedEmail from "@dokploy/server/emails/emails/build-failed";
|
||||
import { renderAsync } from "@react-email/components";
|
||||
import { format } from "date-fns";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
@@ -28,6 +30,7 @@ export const sendBuildErrorNotifications = async ({
|
||||
adminId,
|
||||
}: Props) => {
|
||||
const date = new Date();
|
||||
const unixDate = ~~(Number(date) / 1000);
|
||||
const notificationList = await db.query.notifications.findMany({
|
||||
where: and(
|
||||
eq(notifications.appBuildError, true),
|
||||
@@ -38,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({
|
||||
@@ -58,46 +62,49 @@ export const sendBuildErrorNotifications = async ({
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
|
||||
await sendDiscordNotification(discord, {
|
||||
title: "> `⚠️` - Build Failed",
|
||||
title: decorate(">", "`⚠️` Build Failed"),
|
||||
color: 0xed4245,
|
||||
fields: [
|
||||
{
|
||||
name: "`🛠️`・Project",
|
||||
name: decorate("`🛠️`", "Project"),
|
||||
value: projectName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`⚙️`・Application",
|
||||
name: decorate("`⚙️`", "Application"),
|
||||
value: applicationName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`❔`・Type",
|
||||
name: decorate("`❔`", "Type"),
|
||||
value: applicationType,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`📅`・Date",
|
||||
value: date.toLocaleDateString(),
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`⌚`・Time",
|
||||
value: date.toLocaleTimeString(),
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`❓`・Type",
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: "Failed",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`⚠️`・Error Message",
|
||||
name: decorate("`⚠️`", "Error Message"),
|
||||
value: `\`\`\`${errorMessage}\`\`\``,
|
||||
},
|
||||
{
|
||||
name: "`🧷`・Build Link",
|
||||
name: decorate("`🧷`", "Build Link"),
|
||||
value: `[Click here to access build link](${buildLink})`,
|
||||
},
|
||||
],
|
||||
@@ -108,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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 type { Domain } from "@dokploy/server/services/domain";
|
||||
import { renderAsync } from "@react-email/components";
|
||||
import { format } from "date-fns";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
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,8 +28,10 @@ export const sendBuildSuccessNotifications = async ({
|
||||
applicationType,
|
||||
buildLink,
|
||||
adminId,
|
||||
domains,
|
||||
}: Props) => {
|
||||
const date = new Date();
|
||||
const unixDate = ~~(Number(date) / 1000);
|
||||
const notificationList = await db.query.notifications.findMany({
|
||||
where: and(
|
||||
eq(notifications.appDeploy, true),
|
||||
@@ -36,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(
|
||||
@@ -56,42 +63,45 @@ export const sendBuildSuccessNotifications = async ({
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
|
||||
await sendDiscordNotification(discord, {
|
||||
title: "> `✅` - Build Success",
|
||||
title: decorate(">", "`✅` Build Success"),
|
||||
color: 0x57f287,
|
||||
fields: [
|
||||
{
|
||||
name: "`🛠️`・Project",
|
||||
name: decorate("`🛠️`", "Project"),
|
||||
value: projectName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`⚙️`・Application",
|
||||
name: decorate("`⚙️`", "Application"),
|
||||
value: applicationName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`❔`・Application Type",
|
||||
name: decorate("`❔`", "Type"),
|
||||
value: applicationType,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`📅`・Date",
|
||||
value: date.toLocaleDateString(),
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`⌚`・Time",
|
||||
value: date.toLocaleTimeString(),
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`❓`・Type",
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: "Successful",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`🧷`・Build Link",
|
||||
name: decorate("`🧷`", "Build Link"),
|
||||
value: `[Click here to access build link](${buildLink})`,
|
||||
},
|
||||
],
|
||||
@@ -102,19 +112,45 @@ 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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 { format } from "date-fns";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
@@ -26,6 +29,7 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
errorMessage?: string;
|
||||
}) => {
|
||||
const date = new Date();
|
||||
const unixDate = ~~(Number(date) / 1000);
|
||||
const notificationList = await db.query.notifications.findMany({
|
||||
where: and(
|
||||
eq(notifications.databaseBackup, true),
|
||||
@@ -36,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(
|
||||
@@ -61,40 +66,43 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
|
||||
await sendDiscordNotification(discord, {
|
||||
title:
|
||||
type === "success"
|
||||
? "> `✅` - Database Backup Successful"
|
||||
: "> `❌` - Database Backup Failed",
|
||||
? decorate(">", "`✅` Database Backup Successful")
|
||||
: decorate(">", "`❌` Database Backup Failed"),
|
||||
color: type === "success" ? 0x57f287 : 0xed4245,
|
||||
fields: [
|
||||
{
|
||||
name: "`🛠️`・Project",
|
||||
name: decorate("`🛠️`", "Project"),
|
||||
value: projectName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`⚙️`・Application",
|
||||
name: decorate("`⚙️`", "Application"),
|
||||
value: applicationName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`❔`・Database",
|
||||
name: decorate("`❔`", "Database"),
|
||||
value: databaseType,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`📅`・Date",
|
||||
value: date.toLocaleDateString(),
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`⌚`・Time",
|
||||
value: date.toLocaleTimeString(),
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`❓`・Type",
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: type
|
||||
.replace("error", "Failed")
|
||||
.replace("success", "Successful"),
|
||||
@@ -103,7 +111,7 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
...(type === "error" && errorMessage
|
||||
? [
|
||||
{
|
||||
name: "`⚠️`・Error Message",
|
||||
name: decorate("`⚠️`", "Error Message"),
|
||||
value: `\`\`\`${errorMessage}\`\`\``,
|
||||
},
|
||||
]
|
||||
@@ -116,19 +124,35 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@ import { db } from "@dokploy/server/db";
|
||||
import { notifications } from "@dokploy/server/db/schema";
|
||||
import DockerCleanupEmail from "@dokploy/server/emails/emails/docker-cleanup";
|
||||
import { renderAsync } from "@react-email/components";
|
||||
import { format } from "date-fns";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
@@ -15,6 +17,7 @@ export const sendDockerCleanupNotifications = async (
|
||||
message = "Docker cleanup for dokploy",
|
||||
) => {
|
||||
const date = new Date();
|
||||
const unixDate = ~~(Number(date) / 1000);
|
||||
const notificationList = await db.query.notifications.findMany({
|
||||
where: and(
|
||||
eq(notifications.dockerCleanup, true),
|
||||
@@ -25,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(
|
||||
@@ -44,27 +48,30 @@ export const sendDockerCleanupNotifications = async (
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
|
||||
await sendDiscordNotification(discord, {
|
||||
title: "> `✅` - Docker Cleanup",
|
||||
title: decorate(">", "`✅` Docker Cleanup"),
|
||||
color: 0x57f287,
|
||||
fields: [
|
||||
{
|
||||
name: "`📅`・Date",
|
||||
value: date.toLocaleDateString(),
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`⌚`・Time",
|
||||
value: date.toLocaleTimeString(),
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`❓`・Type",
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: "Successful",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`📜`・Message",
|
||||
name: decorate("`📜`", "Message"),
|
||||
value: `\`\`\`${message}\`\`\``,
|
||||
},
|
||||
],
|
||||
@@ -75,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")}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,16 +2,19 @@ import { db } from "@dokploy/server/db";
|
||||
import { notifications } from "@dokploy/server/db/schema";
|
||||
import DokployRestartEmail from "@dokploy/server/emails/emails/dokploy-restart";
|
||||
import { renderAsync } from "@react-email/components";
|
||||
import { format } from "date-fns";
|
||||
import { eq } from "drizzle-orm";
|
||||
import {
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
|
||||
export const sendDokployRestartNotifications = async () => {
|
||||
const date = new Date();
|
||||
const unixDate = ~~(Number(date) / 1000);
|
||||
const notificationList = await db.query.notifications.findMany({
|
||||
where: eq(notifications.dokployRestart, true),
|
||||
with: {
|
||||
@@ -19,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(
|
||||
@@ -33,22 +37,25 @@ export const sendDokployRestartNotifications = async () => {
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
|
||||
await sendDiscordNotification(discord, {
|
||||
title: "> `✅` - Dokploy Server Restarted",
|
||||
title: decorate(">", "`✅` Dokploy Server Restarted"),
|
||||
color: 0x57f287,
|
||||
fields: [
|
||||
{
|
||||
name: "`📅`・Date",
|
||||
value: date.toLocaleDateString(),
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`⌚`・Time",
|
||||
value: date.toLocaleTimeString(),
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`❓`・Type",
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: "Successful",
|
||||
inline: true,
|
||||
},
|
||||
@@ -60,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")}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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}`));
|
||||
}
|
||||
})
|
||||
|
||||
@@ -53,7 +53,7 @@ export const buildRemoteDocker = async (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
const { sourceType, dockerImage, username, password } = application;
|
||||
const { registryUrl, dockerImage, username, password } = application;
|
||||
|
||||
try {
|
||||
if (!dockerImage) {
|
||||
@@ -65,7 +65,7 @@ echo "Pulling ${dockerImage}" >> ${logPath};
|
||||
|
||||
if (username && password) {
|
||||
command += `
|
||||
if ! docker login --username ${username} --password ${password} https://index.docker.io/v1/ >> ${logPath} 2>&1; then
|
||||
if ! echo "${password}" | docker login --username "${username}" --password-stdin "${registryUrl || ""}" >> ${logPath} 2>&1; then
|
||||
echo "❌ Login failed" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
@@ -69,6 +69,7 @@ export const cloneGitRepository = async (
|
||||
});
|
||||
}
|
||||
|
||||
const { port } = sanitizeRepoPathSSH(customGitUrl);
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
@@ -91,7 +92,7 @@ export const cloneGitRepository = async (
|
||||
env: {
|
||||
...process.env,
|
||||
...(customGitSSHKeyId && {
|
||||
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath} -o UserKnownHostsFile=${knownHostsPath}`,
|
||||
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`,
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -168,7 +169,8 @@ export const getCustomGitCloneCommand = async (
|
||||
);
|
||||
if (customGitSSHKeyId) {
|
||||
const sshKey = await findSSHKeyById(customGitSSHKeyId);
|
||||
const gitSshCommand = `ssh -i /tmp/id_rsa -o UserKnownHostsFile=${knownHostsPath}`;
|
||||
const { port } = sanitizeRepoPathSSH(customGitUrl);
|
||||
const gitSshCommand = `ssh -i /tmp/id_rsa${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`;
|
||||
command.push(
|
||||
`
|
||||
echo "${sshKey.privateKey}" > /tmp/id_rsa
|
||||
@@ -304,6 +306,7 @@ export const cloneGitRawRepository = async (entity: {
|
||||
});
|
||||
}
|
||||
|
||||
const { port } = sanitizeRepoPathSSH(customGitUrl);
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
@@ -322,7 +325,7 @@ export const cloneGitRawRepository = async (entity: {
|
||||
env: {
|
||||
...process.env,
|
||||
...(customGitSSHKeyId && {
|
||||
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath} -o UserKnownHostsFile=${knownHostsPath}`,
|
||||
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`,
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -381,7 +384,8 @@ export const cloneRawGitRepositoryRemote = async (compose: Compose) => {
|
||||
command.push(`mkdir -p ${outputPath};`);
|
||||
if (customGitSSHKeyId) {
|
||||
const sshKey = await findSSHKeyById(customGitSSHKeyId);
|
||||
const gitSshCommand = `ssh -i /tmp/id_rsa -o UserKnownHostsFile=${knownHostsPath}`;
|
||||
const { port } = sanitizeRepoPathSSH(customGitUrl);
|
||||
const gitSshCommand = `ssh -i /tmp/id_rsa${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`;
|
||||
command.push(
|
||||
`
|
||||
echo "${sshKey.privateKey}" > /tmp/id_rsa
|
||||
|
||||
@@ -26,7 +26,7 @@ export const refreshGitlabToken = async (gitlabProviderId: string) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch("https://gitlab.com/oauth/token", {
|
||||
const response = await fetch(`${gitlabProvider.gitlabUrl}/oauth/token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
@@ -122,7 +122,7 @@ export const cloneGitlabRepository = async (
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
|
||||
const repoclone = `${gitlab?.gitlabUrl.replace(/^https?:\/\//, "")}/${gitlabPathNamespace}.git`;
|
||||
const cloneUrl = `https://oauth2:${gitlab?.accessToken}@${repoclone}`;
|
||||
|
||||
try {
|
||||
@@ -218,7 +218,7 @@ export const getGitlabCloneCommand = async (
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
|
||||
const repoclone = `${gitlab?.gitlabUrl.replace(/^https?:\/\//, "")}/${gitlabPathNamespace}.git`;
|
||||
const cloneUrl = `https://oauth2:${gitlab?.accessToken}@${repoclone}`;
|
||||
|
||||
const cloneCommand = `
|
||||
@@ -244,7 +244,7 @@ export const getGitlabRepositories = async (gitlabId?: string) => {
|
||||
const gitlabProvider = await findGitlabById(gitlabId);
|
||||
|
||||
const response = await fetch(
|
||||
`https://gitlab.com/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
|
||||
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||
@@ -304,7 +304,7 @@ export const getGitlabBranches = async (input: {
|
||||
const gitlabProvider = await findGitlabById(input.gitlabId);
|
||||
|
||||
const branchesResponse = await fetch(
|
||||
`https://gitlab.com/api/v4/projects/${input.id}/repository/branches`,
|
||||
`${gitlabProvider.gitlabUrl}/api/v4/projects/${input.id}/repository/branches`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||
@@ -350,7 +350,9 @@ export const cloneRawGitlabRepository = async (entity: Compose) => {
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
|
||||
const gitlabUrl = gitlabProvider.gitlabUrl;
|
||||
// What happen with oauth in self hosted instances?
|
||||
const repoclone = `${gitlabUrl.replace(/^https?:\/\//, "")}/${gitlabPathNamespace}.git`;
|
||||
const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`;
|
||||
|
||||
try {
|
||||
@@ -390,7 +392,7 @@ export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
|
||||
await refreshGitlabToken(gitlabId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
|
||||
const repoclone = `${gitlabProvider.gitlabUrl.replace(/^https?:\/\//, "")}/${gitlabPathNamespace}.git`;
|
||||
const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`;
|
||||
try {
|
||||
const command = `
|
||||
@@ -417,7 +419,7 @@ export const testGitlabConnection = async (
|
||||
const gitlabProvider = await findGitlabById(gitlabId);
|
||||
|
||||
const response = await fetch(
|
||||
`https://gitlab.com/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
|
||||
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||
|
||||
Reference in New Issue
Block a user