mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-28 18:45:22 +02:00
Merge branch 'canary' into ulimits-at-0a401843
This commit is contained in:
@@ -71,8 +71,9 @@ export function selectAIProvider(config: { apiUrl: string; apiKey: string }) {
|
||||
return createOpenAICompatible({
|
||||
name: "gemini",
|
||||
baseURL: config.apiUrl,
|
||||
queryParams: { key: config.apiKey },
|
||||
headers: {},
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
},
|
||||
});
|
||||
case "custom":
|
||||
return createOpenAICompatible({
|
||||
@@ -102,7 +103,7 @@ export const getProviderHeaders = (
|
||||
// Mistral
|
||||
if (apiUrl.includes("mistral")) {
|
||||
return {
|
||||
Authorization: apiKey,
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import path from "node:path";
|
||||
import { CLEANUP_CRON_JOB } from "@dokploy/server/constants";
|
||||
import { member } from "@dokploy/server/db/schema";
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import { getAllServers } from "@dokploy/server/services/server";
|
||||
@@ -29,7 +30,7 @@ export const initCronJobs = async () => {
|
||||
const webServerSettings = await getWebServerSettings();
|
||||
|
||||
if (webServerSettings?.enableDockerCleanup) {
|
||||
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
|
||||
scheduleJob("docker-cleanup", CLEANUP_CRON_JOB, async () => {
|
||||
console.log(
|
||||
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
|
||||
);
|
||||
@@ -45,7 +46,7 @@ export const initCronJobs = async () => {
|
||||
for (const server of servers) {
|
||||
const { serverId, enableDockerCleanup, name } = server;
|
||||
if (enableDockerCleanup) {
|
||||
scheduleJob(serverId, "0 0 * * *", async () => {
|
||||
scheduleJob(serverId, CLEANUP_CRON_JOB, async () => {
|
||||
console.log(
|
||||
`SERVER-BACKUP[${new Date().toLocaleString()}] Running Cleanup ${name}`,
|
||||
);
|
||||
|
||||
@@ -134,6 +134,7 @@ const getExportEnvCommand = (compose: ComposeNested) => {
|
||||
const envVars = getEnviromentVariablesObject(
|
||||
compose.env,
|
||||
compose.environment.project.env,
|
||||
compose.environment.env,
|
||||
);
|
||||
const exports = Object.entries(envVars)
|
||||
.map(([key, value]) => `${key}=${quote([value])}`)
|
||||
|
||||
@@ -1,25 +1,6 @@
|
||||
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { encodeBase64, prepareEnvironmentVariables } from "../docker/utils";
|
||||
|
||||
export const createEnvFile = (
|
||||
directory: string,
|
||||
env: string | null,
|
||||
projectEnv?: string | null,
|
||||
environmentEnv?: string | null,
|
||||
) => {
|
||||
const envFilePath = join(dirname(directory), ".env");
|
||||
if (!existsSync(dirname(envFilePath))) {
|
||||
mkdirSync(dirname(envFilePath), { recursive: true });
|
||||
}
|
||||
const envFileContent = prepareEnvironmentVariables(
|
||||
env,
|
||||
projectEnv,
|
||||
environmentEnv,
|
||||
).join("\n");
|
||||
writeFileSync(envFilePath, envFileContent);
|
||||
};
|
||||
|
||||
export const createEnvFileCommand = (
|
||||
directory: string,
|
||||
env: string | null,
|
||||
|
||||
68
packages/server/src/utils/crons/enterprise.ts
Normal file
68
packages/server/src/utils/crons/enterprise.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { getPublicIpWithFallback } from "@dokploy/server/wss/utils";
|
||||
import { and, eq, isNotNull } from "drizzle-orm";
|
||||
import { scheduleJob } from "node-schedule";
|
||||
import { db } from "../../db/index";
|
||||
import { user as userSchema } from "../../db/schema/user";
|
||||
|
||||
export const LICENSE_KEY_URL =
|
||||
process.env.NODE_ENV === "development"
|
||||
? "http://localhost:4002"
|
||||
: "https://licenses.dokploy.com";
|
||||
|
||||
export const initEnterpriseBackupCronJobs = async () => {
|
||||
scheduleJob("enterprise-check", "0 0 */3 * *", async () => {
|
||||
const users = await db.query.user.findMany({
|
||||
where: and(
|
||||
isNotNull(userSchema.licenseKey),
|
||||
isNotNull(userSchema.enableEnterpriseFeatures),
|
||||
eq(userSchema.isValidEnterpriseLicense, true),
|
||||
),
|
||||
});
|
||||
for (const user of users) {
|
||||
if (user.isValidEnterpriseLicense) {
|
||||
console.log(
|
||||
"Validating license key....",
|
||||
user.firstName,
|
||||
user.lastName,
|
||||
);
|
||||
try {
|
||||
const isValid = await validateLicenseKey(user.licenseKey || "");
|
||||
if (!isValid) {
|
||||
throw new Error("License key is invalid");
|
||||
}
|
||||
} catch (error) {
|
||||
await db
|
||||
.update(userSchema)
|
||||
.set({ isValidEnterpriseLicense: false })
|
||||
.where(eq(userSchema.id, user.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const validateLicenseKey = async (licenseKey: string) => {
|
||||
try {
|
||||
const ip = await getPublicIpWithFallback();
|
||||
const result = await fetch(`${LICENSE_KEY_URL}/licenses/validate`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ licenseKey, ip }),
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
const errorData = await result.json().catch(() => ({}));
|
||||
throw new Error(errorData.message || "Failed to validate license key");
|
||||
}
|
||||
|
||||
const data = await result.json();
|
||||
return data.valid;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
error instanceof Error ? error.message : "Failed to validate license key",
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -330,6 +330,7 @@ export const addDokployNetworkToService = (
|
||||
) => {
|
||||
let networks = networkService;
|
||||
const network = "dokploy-network";
|
||||
const defaultNetwork = "default";
|
||||
if (!networks) {
|
||||
networks = [];
|
||||
}
|
||||
@@ -338,10 +339,16 @@ export const addDokployNetworkToService = (
|
||||
if (!networks.includes(network)) {
|
||||
networks.push(network);
|
||||
}
|
||||
if (!networks.includes(defaultNetwork)) {
|
||||
networks.push(defaultNetwork);
|
||||
}
|
||||
} else if (networks && typeof networks === "object") {
|
||||
if (!(network in networks)) {
|
||||
networks[network] = {};
|
||||
}
|
||||
if (!(defaultNetwork in networks)) {
|
||||
networks[defaultNetwork] = {};
|
||||
}
|
||||
}
|
||||
|
||||
return networks;
|
||||
|
||||
@@ -102,7 +102,8 @@ export const removeMonitoringDirectory = async (
|
||||
};
|
||||
|
||||
export const getBuildAppDirectory = (application: Application) => {
|
||||
const { APPLICATIONS_PATH } = paths(!!application.serverId);
|
||||
const serverId = application.buildServerId || application.serverId;
|
||||
const { APPLICATIONS_PATH } = paths(!!serverId);
|
||||
const { appName, buildType, sourceType, customGitBuildPath, dockerfile } =
|
||||
application;
|
||||
let buildPath = "";
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
@@ -44,18 +46,30 @@ export const sendBuildErrorNotifications = async ({
|
||||
discord: true,
|
||||
telegram: true,
|
||||
slack: true,
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const { email, discord, telegram, slack, gotify, ntfy, custom, lark } =
|
||||
notification;
|
||||
const {
|
||||
email,
|
||||
resend,
|
||||
discord,
|
||||
telegram,
|
||||
slack,
|
||||
gotify,
|
||||
ntfy,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
} = notification;
|
||||
try {
|
||||
if (email) {
|
||||
if (email || resend) {
|
||||
const template = await renderAsync(
|
||||
BuildFailedEmail({
|
||||
projectName,
|
||||
@@ -66,11 +80,22 @@ export const sendBuildErrorNotifications = async ({
|
||||
date: date.toLocaleString(),
|
||||
}),
|
||||
).catch();
|
||||
await sendEmailNotification(
|
||||
email,
|
||||
"Build failed for dokploy",
|
||||
template,
|
||||
);
|
||||
|
||||
if (email) {
|
||||
await sendEmailNotification(
|
||||
email,
|
||||
"Build failed for dokploy",
|
||||
template,
|
||||
);
|
||||
}
|
||||
|
||||
if (resend) {
|
||||
await sendResendNotification(
|
||||
resend,
|
||||
"Build failed for dokploy",
|
||||
template,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
@@ -349,6 +374,14 @@ export const sendBuildErrorNotifications = async ({
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (pushover) {
|
||||
await sendPushoverNotification(
|
||||
pushover,
|
||||
"Build Failed",
|
||||
`Project: ${projectName}\nApplication: ${applicationName}\nType: ${applicationType}\nDate: ${date.toLocaleString()}\nError: ${errorMessage}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
@@ -47,18 +49,30 @@ export const sendBuildSuccessNotifications = async ({
|
||||
discord: true,
|
||||
telegram: true,
|
||||
slack: true,
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const { email, discord, telegram, slack, gotify, ntfy, custom, lark } =
|
||||
notification;
|
||||
const {
|
||||
email,
|
||||
resend,
|
||||
discord,
|
||||
telegram,
|
||||
slack,
|
||||
gotify,
|
||||
ntfy,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
} = notification;
|
||||
try {
|
||||
if (email) {
|
||||
if (email || resend) {
|
||||
const template = await renderAsync(
|
||||
BuildSuccessEmail({
|
||||
projectName,
|
||||
@@ -69,11 +83,22 @@ export const sendBuildSuccessNotifications = async ({
|
||||
environmentName,
|
||||
}),
|
||||
).catch();
|
||||
await sendEmailNotification(
|
||||
email,
|
||||
"Build success for dokploy",
|
||||
template,
|
||||
);
|
||||
|
||||
if (email) {
|
||||
await sendEmailNotification(
|
||||
email,
|
||||
"Build success for dokploy",
|
||||
template,
|
||||
);
|
||||
}
|
||||
|
||||
if (resend) {
|
||||
await sendResendNotification(
|
||||
resend,
|
||||
"Build success for dokploy",
|
||||
template,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
@@ -363,6 +388,14 @@ export const sendBuildSuccessNotifications = async ({
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (pushover) {
|
||||
await sendPushoverNotification(
|
||||
pushover,
|
||||
"Build Success",
|
||||
`Project: ${projectName}\nApplication: ${applicationName}\nEnvironment: ${environmentName}\nType: ${applicationType}\nDate: ${date.toLocaleString()}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
@@ -44,18 +46,30 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
discord: true,
|
||||
telegram: true,
|
||||
slack: true,
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const { email, discord, telegram, slack, gotify, ntfy, custom, lark } =
|
||||
notification;
|
||||
const {
|
||||
email,
|
||||
resend,
|
||||
discord,
|
||||
telegram,
|
||||
slack,
|
||||
gotify,
|
||||
ntfy,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
} = notification;
|
||||
try {
|
||||
if (email) {
|
||||
if (email || resend) {
|
||||
const template = await renderAsync(
|
||||
DatabaseBackupEmail({
|
||||
projectName,
|
||||
@@ -66,11 +80,22 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
date: date.toLocaleString(),
|
||||
}),
|
||||
).catch();
|
||||
await sendEmailNotification(
|
||||
email,
|
||||
"Database backup for dokploy",
|
||||
template,
|
||||
);
|
||||
|
||||
if (email) {
|
||||
await sendEmailNotification(
|
||||
email,
|
||||
"Database backup for dokploy",
|
||||
template,
|
||||
);
|
||||
}
|
||||
|
||||
if (resend) {
|
||||
await sendResendNotification(
|
||||
resend,
|
||||
"Database backup for dokploy",
|
||||
template,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
@@ -377,6 +402,14 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (pushover) {
|
||||
await sendPushoverNotification(
|
||||
pushover,
|
||||
`Database Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
`Project: ${projectName}\nApplication: ${applicationName}\nDatabase: ${databaseType}\nDatabase Name: ${databaseName}\nDate: ${date.toLocaleString()}${type === "error" && errorMessage ? `\nError: ${errorMessage}` : ""}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
@@ -31,27 +33,49 @@ export const sendDockerCleanupNotifications = async (
|
||||
discord: true,
|
||||
telegram: true,
|
||||
slack: true,
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const { email, discord, telegram, slack, gotify, ntfy, custom, lark } =
|
||||
notification;
|
||||
const {
|
||||
email,
|
||||
resend,
|
||||
discord,
|
||||
telegram,
|
||||
slack,
|
||||
gotify,
|
||||
ntfy,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
} = notification;
|
||||
try {
|
||||
if (email) {
|
||||
if (email || resend) {
|
||||
const template = await renderAsync(
|
||||
DockerCleanupEmail({ message, date: date.toLocaleString() }),
|
||||
).catch();
|
||||
|
||||
await sendEmailNotification(
|
||||
email,
|
||||
"Docker cleanup for dokploy",
|
||||
template,
|
||||
);
|
||||
if (email) {
|
||||
await sendEmailNotification(
|
||||
email,
|
||||
"Docker cleanup for dokploy",
|
||||
template,
|
||||
);
|
||||
}
|
||||
|
||||
if (resend) {
|
||||
await sendResendNotification(
|
||||
resend,
|
||||
"Docker cleanup for dokploy",
|
||||
template,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
@@ -230,6 +254,14 @@ export const sendDockerCleanupNotifications = async (
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (pushover) {
|
||||
await sendPushoverNotification(
|
||||
pushover,
|
||||
"Docker Cleanup",
|
||||
`Date: ${date.toLocaleString()}\nMessage: ${message}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
@@ -25,28 +27,50 @@ export const sendDokployRestartNotifications = async () => {
|
||||
discord: true,
|
||||
telegram: true,
|
||||
slack: true,
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const { email, discord, telegram, slack, gotify, ntfy, custom, lark } =
|
||||
notification;
|
||||
const {
|
||||
email,
|
||||
resend,
|
||||
discord,
|
||||
telegram,
|
||||
slack,
|
||||
gotify,
|
||||
ntfy,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
} = notification;
|
||||
|
||||
try {
|
||||
if (email) {
|
||||
if (email || resend) {
|
||||
const template = await renderAsync(
|
||||
DokployRestartEmail({ date: date.toLocaleString() }),
|
||||
).catch();
|
||||
|
||||
await sendEmailNotification(
|
||||
email,
|
||||
"Dokploy Server Restarted",
|
||||
template,
|
||||
);
|
||||
if (email) {
|
||||
await sendEmailNotification(
|
||||
email,
|
||||
"Dokploy Server Restarted",
|
||||
template,
|
||||
);
|
||||
}
|
||||
|
||||
if (resend) {
|
||||
await sendResendNotification(
|
||||
resend,
|
||||
"Dokploy Server Restarted",
|
||||
template,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
@@ -219,6 +243,14 @@ export const sendDokployRestartNotifications = async () => {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (pushover) {
|
||||
await sendPushoverNotification(
|
||||
pushover,
|
||||
"Dokploy Server Restarted",
|
||||
`Date: ${date.toLocaleString()}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
sendCustomNotification,
|
||||
sendDiscordNotification,
|
||||
sendLarkNotification,
|
||||
sendPushoverNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
@@ -38,6 +39,7 @@ export const sendServerThresholdNotifications = async (
|
||||
slack: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -45,7 +47,7 @@ export const sendServerThresholdNotifications = async (
|
||||
const typeColor = 0xff0000; // Rojo para indicar alerta
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const { discord, telegram, slack, custom, lark } = notification;
|
||||
const { discord, telegram, slack, custom, lark, pushover } = notification;
|
||||
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
@@ -266,5 +268,13 @@ export const sendServerThresholdNotifications = async (
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (pushover) {
|
||||
await sendPushoverNotification(
|
||||
pushover,
|
||||
`Server ${payload.Type} Alert`,
|
||||
`Server: ${payload.ServerName}\nType: ${payload.Type}\nCurrent: ${payload.Value.toFixed(2)}%\nThreshold: ${payload.Threshold.toFixed(2)}%\nMessage: ${payload.Message}\nTime: ${date.toLocaleString()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,10 +5,13 @@ import type {
|
||||
gotify,
|
||||
lark,
|
||||
ntfy,
|
||||
pushover,
|
||||
resend,
|
||||
slack,
|
||||
telegram,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import nodemailer from "nodemailer";
|
||||
import { Resend } from "resend";
|
||||
|
||||
export const sendEmailNotification = async (
|
||||
connection: typeof email.$inferInsert,
|
||||
@@ -45,6 +48,32 @@ export const sendEmailNotification = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const sendResendNotification = async (
|
||||
connection: typeof resend.$inferInsert,
|
||||
subject: string,
|
||||
htmlContent: string,
|
||||
) => {
|
||||
try {
|
||||
const client = new Resend(connection.apiKey);
|
||||
|
||||
const result = await client.emails.send({
|
||||
from: connection.fromAddress,
|
||||
to: connection.toAddresses,
|
||||
subject,
|
||||
html: htmlContent,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
throw new Error(
|
||||
`Failed to send Resend notification ${err instanceof Error ? err.message : "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const sendDiscordNotification = async (
|
||||
connection: typeof discord.$inferInsert,
|
||||
embed: any,
|
||||
@@ -223,3 +252,33 @@ export const sendLarkNotification = async (
|
||||
console.log(err);
|
||||
}
|
||||
};
|
||||
|
||||
export const sendPushoverNotification = async (
|
||||
connection: typeof pushover.$inferInsert,
|
||||
title: string,
|
||||
message: string,
|
||||
) => {
|
||||
const formData = new URLSearchParams();
|
||||
formData.append("token", connection.apiToken);
|
||||
formData.append("user", connection.userKey);
|
||||
formData.append("title", title);
|
||||
formData.append("message", message);
|
||||
formData.append("priority", connection.priority?.toString() || "0");
|
||||
|
||||
// For emergency priority (2), retry and expire are required
|
||||
if (connection.priority === 2) {
|
||||
formData.append("retry", connection.retry?.toString() || "30");
|
||||
formData.append("expire", connection.expire?.toString() || "3600");
|
||||
}
|
||||
|
||||
const response = await fetch("https://api.pushover.net/1/messages.json", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to send Pushover notification: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
@@ -51,15 +53,18 @@ export const sendVolumeBackupNotifications = async ({
|
||||
discord: true,
|
||||
telegram: true,
|
||||
slack: true,
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
pushover: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const { email, discord, telegram, slack, gotify, ntfy } = notification;
|
||||
const { email, resend, discord, telegram, slack, gotify, ntfy, pushover } =
|
||||
notification;
|
||||
|
||||
if (email) {
|
||||
if (email || resend) {
|
||||
const subject = `Volume Backup ${type === "success" ? "Successful" : "Failed"} - ${applicationName}`;
|
||||
const htmlContent = await renderAsync(
|
||||
VolumeBackupEmail({
|
||||
@@ -73,7 +78,12 @@ export const sendVolumeBackupNotifications = async ({
|
||||
date: date.toISOString(),
|
||||
}),
|
||||
);
|
||||
await sendEmailNotification(email, subject, htmlContent);
|
||||
if (email) {
|
||||
await sendEmailNotification(email, subject, htmlContent);
|
||||
}
|
||||
if (resend) {
|
||||
await sendResendNotification(resend, subject, htmlContent);
|
||||
}
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
@@ -270,5 +280,13 @@ export const sendVolumeBackupNotifications = async ({
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (pushover) {
|
||||
await sendPushoverNotification(
|
||||
pushover,
|
||||
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
`Project: ${projectName}\nApplication: ${applicationName}\nVolume: ${volumeName}\nService Type: ${serviceType}${backupSize ? `\nBackup Size: ${backupSize}` : ""}\nDate: ${date.toLocaleString()}${type === "error" && errorMessage ? `\nError: ${errorMessage}` : ""}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -79,6 +79,7 @@ export const getBitbucketHeaders = (bitbucketProvider: Bitbucket) => {
|
||||
interface CloneBitbucketRepository {
|
||||
appName: string;
|
||||
bitbucketRepository: string | null;
|
||||
bitbucketRepositorySlug?: string | null;
|
||||
bitbucketOwner: string | null;
|
||||
bitbucketBranch: string | null;
|
||||
bitbucketId: string | null;
|
||||
@@ -117,7 +118,8 @@ export const cloneBitbucketRepository = async ({
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
command += `rm -rf ${outputPath};`;
|
||||
command += `mkdir -p ${outputPath};`;
|
||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
||||
const repoToUse = entity.bitbucketRepositorySlug || bitbucketRepository;
|
||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${repoToUse}.git`;
|
||||
const cloneUrl = getBitbucketCloneUrl(bitbucket, repoclone);
|
||||
command += `echo "Cloning Repo ${repoclone} to ${outputPath}: ✅";`;
|
||||
command += `git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`;
|
||||
@@ -137,6 +139,7 @@ export const getBitbucketRepositories = async (bitbucketId?: string) => {
|
||||
let repositories: {
|
||||
name: string;
|
||||
url: string;
|
||||
slug: string;
|
||||
owner: { username: string };
|
||||
}[] = [];
|
||||
|
||||
@@ -159,6 +162,7 @@ export const getBitbucketRepositories = async (bitbucketId?: string) => {
|
||||
const mappedData = data.values.map((repo: any) => ({
|
||||
name: repo.name,
|
||||
url: repo.links.html.href,
|
||||
slug: repo.slug,
|
||||
owner: {
|
||||
username: repo.workspace.slug,
|
||||
},
|
||||
|
||||
@@ -49,7 +49,9 @@ export const refreshGiteaToken = async (giteaProviderId: string) => {
|
||||
}
|
||||
|
||||
// Token is expired or about to expire, refresh it
|
||||
const tokenEndpoint = `${giteaProvider.giteaUrl}/login/oauth/access_token`;
|
||||
// Use internal URL when Gitea is on same instance as Dokploy
|
||||
const baseUrl = giteaProvider.giteaInternalUrl || giteaProvider.giteaUrl;
|
||||
const tokenEndpoint = `${baseUrl}/login/oauth/access_token`;
|
||||
const params = new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: giteaProvider.refreshToken,
|
||||
|
||||
@@ -21,7 +21,9 @@ export const refreshGitlabToken = async (gitlabProviderId: string) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(`${gitlabProvider.gitlabUrl}/oauth/token`, {
|
||||
// Use internal URL for token refresh when GitLab is on same instance as Dokploy
|
||||
const baseUrl = gitlabProvider.gitlabInternalUrl || gitlabProvider.gitlabUrl;
|
||||
const response = await fetch(`${baseUrl}/oauth/token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import { IS_CLOUD, paths } from "@dokploy/server/constants";
|
||||
import type { Schedule } from "@dokploy/server/db/schema/schedule";
|
||||
import {
|
||||
createDeploymentSchedule,
|
||||
@@ -93,6 +93,13 @@ export const runCommand = async (scheduleId: string) => {
|
||||
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
|
||||
|
||||
try {
|
||||
if (IS_CLOUD) {
|
||||
writeStream.write(
|
||||
"This feature is not available in the cloud version.",
|
||||
);
|
||||
writeStream.end();
|
||||
return;
|
||||
}
|
||||
writeStream.write(
|
||||
`docker exec ${containerId} ${shellType} -c ${command}\n`,
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ export const backupVolume = async (
|
||||
const { serviceType, volumeName, turnOff, prefix } = volumeBackup;
|
||||
const serverId =
|
||||
volumeBackup.application?.serverId || volumeBackup.compose?.serverId;
|
||||
const { VOLUME_BACKUPS_PATH } = paths(!!serverId);
|
||||
const { VOLUME_BACKUPS_PATH, VOLUME_BACKUP_LOCK_PATH } = paths(!!serverId);
|
||||
const destination = volumeBackup.destination;
|
||||
const backupFileName = `${volumeName}-${new Date().toISOString()}.tar`;
|
||||
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
@@ -45,16 +45,56 @@ export const backupVolume = async (
|
||||
return baseCommand;
|
||||
}
|
||||
|
||||
const serviceLockId =
|
||||
serviceType === "application"
|
||||
? volumeBackup.application?.appName
|
||||
: `${volumeBackup.compose?.appName}_${volumeBackup.serviceName}`;
|
||||
|
||||
const lockPath = `${VOLUME_BACKUP_LOCK_PATH}-${serviceLockId}`;
|
||||
|
||||
const lockWrapper = (body: string) => `
|
||||
set -e
|
||||
|
||||
LOCK_PATH="${lockPath}"
|
||||
|
||||
echo "Waiting for volume backup lock: $LOCK_PATH"
|
||||
|
||||
if command -v flock >/dev/null 2>&1; then
|
||||
exec 9>"$LOCK_PATH"
|
||||
flock 9
|
||||
else
|
||||
LOCK_DIR="$LOCK_PATH.dir"
|
||||
while ! mkdir "$LOCK_DIR" 2>/dev/null; do
|
||||
echo "Waiting for volume backup lock: $LOCK_PATH"
|
||||
sleep 5
|
||||
done
|
||||
trap 'rm -rf "$LOCK_DIR"' EXIT
|
||||
fi
|
||||
|
||||
echo "Volume backup lock acquired"
|
||||
|
||||
${body}
|
||||
|
||||
echo "Volume backup lock released"
|
||||
`;
|
||||
|
||||
console.log(
|
||||
lockWrapper(`
|
||||
echo "Volume backup lock acquired"
|
||||
echo "Volume backup lock released"
|
||||
`),
|
||||
);
|
||||
|
||||
if (serviceType === "application") {
|
||||
return `
|
||||
return lockWrapper(`
|
||||
echo "Stopping application to 0 replicas"
|
||||
ACTUAL_REPLICAS=$(docker service inspect ${volumeBackup.application?.appName} --format "{{.Spec.Mode.Replicated.Replicas}}")
|
||||
echo "Actual replicas: $ACTUAL_REPLICAS"
|
||||
docker service scale ${volumeBackup.application?.appName}=0
|
||||
docker service update --replicas=0 ${volumeBackup.application?.appName}
|
||||
${baseCommand}
|
||||
echo "Starting application to $ACTUAL_REPLICAS replicas"
|
||||
docker service scale ${volumeBackup.application?.appName}=$ACTUAL_REPLICAS
|
||||
`;
|
||||
docker service update --replicas=$ACTUAL_REPLICAS --with-registry-auth ${volumeBackup.application?.appName}
|
||||
`);
|
||||
}
|
||||
if (serviceType === "compose") {
|
||||
const compose = await findComposeById(
|
||||
@@ -69,25 +109,27 @@ export const backupVolume = async (
|
||||
echo "Service name: ${compose.appName}_${volumeBackup.serviceName}"
|
||||
ACTUAL_REPLICAS=$(docker service inspect ${compose.appName}_${volumeBackup.serviceName} --format "{{.Spec.Mode.Replicated.Replicas}}")
|
||||
echo "Actual replicas: $ACTUAL_REPLICAS"
|
||||
docker service scale ${compose.appName}_${volumeBackup.serviceName}=0`;
|
||||
docker service update --replicas=0 ${compose.appName}_${volumeBackup.serviceName}`;
|
||||
|
||||
startCommand = `
|
||||
echo "Starting compose to $ACTUAL_REPLICAS replicas"
|
||||
docker service scale ${compose.appName}_${volumeBackup.serviceName}=$ACTUAL_REPLICAS`;
|
||||
docker service update --replicas=$ACTUAL_REPLICAS --with-registry-auth ${compose.appName}_${volumeBackup.serviceName}`;
|
||||
} else {
|
||||
stopCommand = `
|
||||
echo "Stopping compose container"
|
||||
ID=$(docker ps -q --filter "label=com.docker.compose.project=${compose.appName}" --filter "label=com.docker.compose.service=${volumeBackup.serviceName}")
|
||||
docker stop $ID`;
|
||||
|
||||
startCommand = `
|
||||
echo "Starting compose container"
|
||||
docker start $ID
|
||||
echo "Compose container started"
|
||||
`;
|
||||
}
|
||||
return `
|
||||
return lockWrapper(`
|
||||
${stopCommand}
|
||||
${baseCommand}
|
||||
${startCommand}
|
||||
`;
|
||||
`);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user