Merge branch 'canary' into feature/delete-docker-volumes

This commit is contained in:
djknaeckebrot
2024-12-23 08:13:12 +01:00
76 changed files with 3638 additions and 861 deletions

View File

@@ -1,3 +1,4 @@
import { generatePassword } from "@dokploy/server/templates/utils";
import { faker } from "@faker-js/faker";
import { customAlphabet } from "nanoid";
@@ -13,3 +14,17 @@ export const generateAppName = (type: string) => {
const nanoidPart = customNanoid();
return `${type}-${randomFakerElement}-${nanoidPart}`;
};
export const cleanAppName = (appName?: string) => {
if (!appName) {
return appName?.toLowerCase();
}
return appName.trim().replace(/ /g, "-").toLowerCase();
};
export const buildAppName = (type: string, baseAppName?: string) => {
if (baseAppName) {
return `${cleanAppName(baseAppName)}-${generatePassword(6)}`;
}
return generateAppName(type);
};

View File

@@ -3,10 +3,10 @@ import { db } from "@dokploy/server/db";
import {
type apiCreateApplication,
applications,
buildAppName,
cleanAppName,
} from "@dokploy/server/db/schema";
import { generateAppName } from "@dokploy/server/db/schema";
import { getAdvancedStats } from "@dokploy/server/monitoring/utilts";
import { generatePassword } from "@dokploy/server/templates/utils";
import {
buildApplication,
getBuildCommand,
@@ -46,34 +46,31 @@ import {
createDeploymentPreview,
updateDeploymentStatus,
} from "./deployment";
import { validUniqueServerAppName } from "./project";
import {
findPreviewDeploymentById,
updatePreviewDeployment,
} from "./preview-deployment";
import { type Domain, getDomainHost } from "./domain";
import {
createPreviewDeploymentComment,
getIssueComment,
issueCommentExists,
updateIssueComment,
} from "./github";
import { type Domain, getDomainHost } from "./domain";
import {
findPreviewDeploymentById,
updatePreviewDeployment,
} from "./preview-deployment";
import { validUniqueServerAppName } from "./project";
export type Application = typeof applications.$inferSelect;
export const createApplication = async (
input: typeof apiCreateApplication._type,
) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("app");
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
const appName = buildAppName("app", input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Application with this 'AppName' already exists",
});
}
const valid = await validUniqueServerAppName(appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Application with this 'AppName' already exists",
});
}
return await db.transaction(async (tx) => {
@@ -81,6 +78,7 @@ export const createApplication = async (
.insert(applications)
.values({
...input,
appName,
})
.returning()
.then((value) => value[0]);
@@ -140,10 +138,11 @@ export const updateApplication = async (
applicationId: string,
applicationData: Partial<Application>,
) => {
const { appName, ...rest } = applicationData;
const application = await db
.update(applications)
.set({
...applicationData,
...rest,
})
.where(eq(applications.applicationId, applicationId))
.returning();

View File

@@ -2,7 +2,7 @@ import { join } from "node:path";
import { paths } from "@dokploy/server/constants";
import { db } from "@dokploy/server/db";
import { type apiCreateCompose, compose } from "@dokploy/server/db/schema";
import { generateAppName } from "@dokploy/server/db/schema";
import { buildAppName, cleanAppName } from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates/utils";
import {
buildCompose,
@@ -52,17 +52,14 @@ import { validUniqueServerAppName } from "./project";
export type Compose = typeof compose.$inferSelect;
export const createCompose = async (input: typeof apiCreateCompose._type) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("compose");
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
const appName = buildAppName("compose", input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
const valid = await validUniqueServerAppName(appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
const newDestination = await db
@@ -70,6 +67,7 @@ export const createCompose = async (input: typeof apiCreateCompose._type) => {
.values({
...input,
composeFile: "",
appName,
})
.returning()
.then((value) => value[0]);
@@ -87,8 +85,9 @@ export const createCompose = async (input: typeof apiCreateCompose._type) => {
export const createComposeByTemplate = async (
input: typeof compose.$inferInsert,
) => {
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
const appName = cleanAppName(input.appName);
if (appName) {
const valid = await validUniqueServerAppName(appName);
if (!valid) {
throw new TRPCError({
@@ -101,6 +100,7 @@ export const createComposeByTemplate = async (
.insert(compose)
.values({
...input,
appName,
})
.returning()
.then((value) => value[0]);
@@ -184,10 +184,11 @@ export const updateCompose = async (
composeId: string,
composeData: Partial<Compose>,
) => {
const { appName, ...rest } = composeData;
const composeResult = await db
.update(compose)
.set({
...composeData,
...rest,
})
.where(eq(compose.composeId, composeId))
.returning();

View File

@@ -23,8 +23,8 @@ import { type Server, findServerById } from "./server";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import {
findPreviewDeploymentById,
type PreviewDeployment,
findPreviewDeploymentById,
updatePreviewDeployment,
} from "./preview-deployment";

View File

@@ -224,3 +224,124 @@ export const containerRestart = async (containerId: string) => {
return config;
} catch (error) {}
};
export const getSwarmNodes = async (serverId?: string) => {
try {
let stdout = "";
let stderr = "";
const command = "docker node ls --format '{{json .}}'";
if (serverId) {
const result = await execAsyncRemote(serverId, command);
stdout = result.stdout;
stderr = result.stderr;
} else {
const result = await execAsync(command);
stdout = result.stdout;
stderr = result.stderr;
}
if (stderr) {
console.error(`Error: ${stderr}`);
return;
}
const nodes = JSON.parse(stdout);
const nodesArray = stdout
.trim()
.split("\n")
.map((line) => JSON.parse(line));
return nodesArray;
} catch (error) {}
};
export const getNodeInfo = async (nodeId: string, serverId?: string) => {
try {
const command = `docker node inspect ${nodeId} --format '{{json .}}'`;
let stdout = "";
let stderr = "";
if (serverId) {
const result = await execAsyncRemote(serverId, command);
stdout = result.stdout;
stderr = result.stderr;
} else {
const result = await execAsync(command);
stdout = result.stdout;
stderr = result.stderr;
}
if (stderr) {
console.error(`Error: ${stderr}`);
return;
}
const nodeInfo = JSON.parse(stdout);
return nodeInfo;
} catch (error) {}
};
export const getNodeApplications = async (serverId?: string) => {
try {
let stdout = "";
let stderr = "";
const command = `docker service ls --format '{{json .}}'`;
if (serverId) {
const result = await execAsyncRemote(serverId, command);
stdout = result.stdout;
stderr = result.stderr;
} else {
const result = await execAsync(command);
stdout = result.stdout;
stderr = result.stderr;
}
if (stderr) {
console.error(`Error: ${stderr}`);
return;
}
const appArray = stdout
.trim()
.split("\n")
.map((line) => JSON.parse(line));
return appArray;
} catch (error) {}
};
export const getApplicationInfo = async (
appName: string,
serverId?: string,
) => {
try {
let stdout = "";
let stderr = "";
const command = `docker service ps ${appName} --format '{{json .}}'`;
if (serverId) {
const result = await execAsyncRemote(serverId, command);
stdout = result.stdout;
stderr = result.stderr;
} else {
const result = await execAsync(command);
stdout = result.stdout;
stderr = result.stderr;
}
if (stderr) {
console.error(`Error: ${stderr}`);
return;
}
const appArray = stdout
.trim()
.split("\n")
.map((line) => JSON.parse(line));
return appArray;
} catch (error) {}
};

View File

@@ -4,7 +4,7 @@ import {
backups,
mariadb,
} from "@dokploy/server/db/schema";
import { generateAppName } from "@dokploy/server/db/schema";
import { buildAppName, cleanAppName } from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates/utils";
import { buildMariadb } from "@dokploy/server/utils/databases/mariadb";
import { pullImage } from "@dokploy/server/utils/docker/utils";
@@ -17,17 +17,14 @@ import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
export type Mariadb = typeof mariadb.$inferSelect;
export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("mariadb");
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
const appName = buildAppName("mariadb", input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
const valid = await validUniqueServerAppName(input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
const newMariadb = await db
@@ -40,6 +37,7 @@ export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
databaseRootPassword: input.databaseRootPassword
? input.databaseRootPassword
: generatePassword(),
appName,
})
.returning()
.then((value) => value[0]);
@@ -82,10 +80,11 @@ export const updateMariadbById = async (
mariadbId: string,
mariadbData: Partial<Mariadb>,
) => {
const { appName, ...rest } = mariadbData;
const result = await db
.update(mariadb)
.set({
...mariadbData,
...rest,
})
.where(eq(mariadb.mariadbId, mariadbId))
.returning();

View File

@@ -1,6 +1,6 @@
import { db } from "@dokploy/server/db";
import { type apiCreateMongo, backups, mongo } from "@dokploy/server/db/schema";
import { generateAppName } from "@dokploy/server/db/schema";
import { buildAppName, cleanAppName } from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates/utils";
import { buildMongo } from "@dokploy/server/utils/databases/mongo";
import { pullImage } from "@dokploy/server/utils/docker/utils";
@@ -13,17 +13,14 @@ import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
export type Mongo = typeof mongo.$inferSelect;
export const createMongo = async (input: typeof apiCreateMongo._type) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("mongo");
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
const appName = buildAppName("mongo", input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
const valid = await validUniqueServerAppName(appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
const newMongo = await db
@@ -33,6 +30,7 @@ export const createMongo = async (input: typeof apiCreateMongo._type) => {
databasePassword: input.databasePassword
? input.databasePassword
: generatePassword(),
appName,
})
.returning()
.then((value) => value[0]);
@@ -74,10 +72,11 @@ export const updateMongoById = async (
mongoId: string,
mongoData: Partial<Mongo>,
) => {
const { appName, ...rest } = mongoData;
const result = await db
.update(mongo)
.set({
...mongoData,
...rest,
})
.where(eq(mongo.mongoId, mongoId))
.returning();

View File

@@ -1,6 +1,6 @@
import { db } from "@dokploy/server/db";
import { type apiCreateMySql, backups, mysql } from "@dokploy/server/db/schema";
import { generateAppName } from "@dokploy/server/db/schema";
import { buildAppName, cleanAppName } 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";
@@ -13,18 +13,14 @@ import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
export type MySql = typeof mysql.$inferSelect;
export const createMysql = async (input: typeof apiCreateMySql._type) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("mysql");
const appName = buildAppName("mysql", input.appName);
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
const valid = await validUniqueServerAppName(appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
const newMysql = await db
@@ -37,6 +33,7 @@ export const createMysql = async (input: typeof apiCreateMySql._type) => {
databaseRootPassword: input.databaseRootPassword
? input.databaseRootPassword
: generatePassword(),
appName,
})
.returning()
.then((value) => value[0]);
@@ -79,10 +76,11 @@ export const updateMySqlById = async (
mysqlId: string,
mysqlData: Partial<MySql>,
) => {
const { appName, ...rest } = mysqlData;
const result = await db
.update(mysql)
.set({
...mysqlData,
...rest,
})
.where(eq(mysql.mysqlId, mysqlId))
.returning();

View File

@@ -4,7 +4,7 @@ import {
backups,
postgres,
} from "@dokploy/server/db/schema";
import { generateAppName } from "@dokploy/server/db/schema";
import { buildAppName, cleanAppName } from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates/utils";
import { buildPostgres } from "@dokploy/server/utils/databases/postgres";
import { pullImage } from "@dokploy/server/utils/docker/utils";
@@ -17,17 +17,14 @@ import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
export type Postgres = typeof postgres.$inferSelect;
export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("postgres");
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
const appName = buildAppName("postgres", input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
const valid = await validUniqueServerAppName(appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
const newPostgres = await db
@@ -37,6 +34,7 @@ export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
databasePassword: input.databasePassword
? input.databasePassword
: generatePassword(),
appName,
})
.returning()
.then((value) => value[0]);
@@ -96,10 +94,11 @@ export const updatePostgresById = async (
postgresId: string,
postgresData: Partial<Postgres>,
) => {
const { appName, ...rest } = postgresData;
const result = await db
.update(postgres)
.set({
...postgresData,
...rest,
})
.where(eq(postgres.postgresId, postgresId))
.returning();

View File

@@ -7,20 +7,20 @@ import {
import { TRPCError } from "@trpc/server";
import { and, desc, eq } from "drizzle-orm";
import { slugify } from "../setup/server-setup";
import { findApplicationById } from "./application";
import { createDomain } from "./domain";
import { generatePassword, generateRandomDomain } from "../templates/utils";
import { removeService } from "../utils/docker/utils";
import { removeDirectoryCode } from "../utils/filesystem/directory";
import { authGithub } from "../utils/providers/github";
import { removeTraefikConfig } from "../utils/traefik/application";
import { manageDomain } from "../utils/traefik/domain";
import { findAdminById } from "./admin";
import { findApplicationById } from "./application";
import {
removeDeployments,
removeDeploymentsByPreviewDeploymentId,
} from "./deployment";
import { removeDirectoryCode } from "../utils/filesystem/directory";
import { removeTraefikConfig } from "../utils/traefik/application";
import { removeService } from "../utils/docker/utils";
import { authGithub } from "../utils/providers/github";
import { getIssueComment, type Github } from "./github";
import { findAdminById } from "./admin";
import { createDomain } from "./domain";
import { type Github, getIssueComment } from "./github";
export type PreviewDeployment = typeof previewDeployments.$inferSelect;

View File

@@ -1,6 +1,6 @@
import { db } from "@dokploy/server/db";
import { type apiCreateRedis, redis } from "@dokploy/server/db/schema";
import { generateAppName } from "@dokploy/server/db/schema";
import { buildAppName, cleanAppName } from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates/utils";
import { buildRedis } from "@dokploy/server/utils/databases/redis";
import { pullImage } from "@dokploy/server/utils/docker/utils";
@@ -14,17 +14,14 @@ export type Redis = typeof redis.$inferSelect;
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
export const createRedis = async (input: typeof apiCreateRedis._type) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("redis");
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
const appName = buildAppName("redis", input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
const valid = await validUniqueServerAppName(appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
const newRedis = await db
@@ -34,6 +31,7 @@ export const createRedis = async (input: typeof apiCreateRedis._type) => {
databasePassword: input.databasePassword
? input.databasePassword
: generatePassword(),
appName,
})
.returning()
.then((value) => value[0]);
@@ -70,10 +68,11 @@ export const updateRedisById = async (
redisId: string,
redisData: Partial<Redis>,
) => {
const { appName, ...rest } = redisData;
const result = await db
.update(redis)
.set({
...redisData,
...rest,
})
.where(eq(redis.redisId, redisId))
.returning();

View File

@@ -1,41 +1,108 @@
import { readdirSync } from "node:fs";
import { join } from "node:path";
import { docker } from "@dokploy/server/constants";
import { getServiceContainer } from "@dokploy/server/utils/docker/utils";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import {
execAsync,
execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync";
// import packageInfo from "../../../package.json";
const updateIsAvailable = async () => {
try {
const service = await getServiceContainer("dokploy");
export interface IUpdateData {
latestVersion: string | null;
updateAvailable: boolean;
}
const localImage = await docker.getImage(getDokployImage()).inspect();
return localImage.Id !== service?.ImageID;
} catch (error) {
return false;
}
export const DEFAULT_UPDATE_DATA: IUpdateData = {
latestVersion: null,
updateAvailable: false,
};
/** Returns current Dokploy docker image tag or `latest` by default. */
export const getDokployImageTag = () => {
return process.env.RELEASE_TAG || "latest";
};
export const getDokployImage = () => {
return `dokploy/dokploy:${process.env.RELEASE_TAG || "latest"}`;
return `dokploy/dokploy:${getDokployImageTag()}`;
};
export const pullLatestRelease = async () => {
try {
const stream = await docker.pull(getDokployImage(), {});
await new Promise((resolve, reject) => {
docker.modem.followProgress(stream, (err, res) =>
err ? reject(err) : resolve(res),
);
});
const newUpdateIsAvailable = await updateIsAvailable();
return newUpdateIsAvailable;
} catch (error) {}
return false;
const stream = await docker.pull(getDokployImage());
await new Promise((resolve, reject) => {
docker.modem.followProgress(stream, (err, res) =>
err ? reject(err) : resolve(res),
);
});
};
export const getDokployVersion = () => {
// return packageInfo.version;
/** Returns Dokploy docker service image digest */
export const getServiceImageDigest = async () => {
const { stdout } = await execAsync(
"docker service inspect dokploy --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}'",
);
const currentDigest = stdout.trim().split("@")[1];
if (!currentDigest) {
throw new Error("Could not get current service image digest");
}
return currentDigest;
};
/** Returns latest version number and information whether server update is available by comparing current image's digest against digest for provided image tag via Docker hub API. */
export const getUpdateData = async (): Promise<IUpdateData> => {
let currentDigest: string;
try {
currentDigest = await getServiceImageDigest();
} catch {
// Docker service might not exist locally
// You can run the # Installation command for docker service create mentioned in the below docs to test it locally:
// https://docs.dokploy.com/docs/core/manual-installation
return DEFAULT_UPDATE_DATA;
}
const baseUrl = "https://hub.docker.com/v2/repositories/dokploy/dokploy/tags";
let url: string | null = `${baseUrl}?page_size=100`;
let allResults: { digest: string; name: string }[] = [];
while (url) {
const response = await fetch(url, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
const data = (await response.json()) as {
next: string | null;
results: { digest: string; name: string }[];
};
allResults = allResults.concat(data.results);
url = data?.next;
}
const imageTag = getDokployImageTag();
const searchedDigest = allResults.find((t) => t.name === imageTag)?.digest;
if (!searchedDigest) {
return DEFAULT_UPDATE_DATA;
}
if (imageTag === "latest") {
const versionedTag = allResults.find(
(t) => t.digest === searchedDigest && t.name.startsWith("v"),
);
if (!versionedTag) {
return DEFAULT_UPDATE_DATA;
}
const { name: latestVersion, digest } = versionedTag;
const updateAvailable = digest !== currentDigest;
return { latestVersion, updateAvailable };
}
const updateAvailable = searchedDigest !== currentDigest;
return { latestVersion: imageTag, updateAvailable };
};
interface TreeDataItem {

View File

@@ -16,12 +16,18 @@ interface TraefikOptions {
enableDashboard?: boolean;
env?: string[];
serverId?: string;
additionalPorts?: {
targetPort: number;
publishedPort: number;
publishMode?: "ingress" | "host";
}[];
}
export const initializeTraefik = async ({
enableDashboard = false,
env,
serverId,
additionalPorts = [],
}: TraefikOptions = {}) => {
const { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH } = paths(!!serverId);
const imageName = "traefik:v3.1.2";
@@ -84,6 +90,11 @@ export const initializeTraefik = async ({
},
]
: []),
...additionalPorts.map((port) => ({
TargetPort: port.targetPort,
PublishedPort: port.publishedPort,
PublishMode: port.publishMode || ("host" as const),
})),
],
},
};

View File

@@ -11,6 +11,8 @@ import { runMariadbBackup } from "./mariadb";
import { runMongoBackup } from "./mongo";
import { runMySqlBackup } from "./mysql";
import { runPostgresBackup } from "./postgres";
import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
export const initCronJobs = async () => {
console.log("Setting up cron jobs....");
@@ -25,14 +27,15 @@ 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 { appName, serverId, enableDockerCleanup } = server;
if (enableDockerCleanup) {
scheduleJob(serverId, "0 0 * * *", async () => {
console.log(
`SERVER-BACKUP[${new Date().toLocaleString()}] Running Cleanup ${appName}`,
@@ -40,12 +43,17 @@ export const initCronJobs = async () => {
await cleanUpUnusedImages(serverId);
await cleanUpDockerBuilder(serverId);
await cleanUpSystemPrune(serverId);
await sendDockerCleanupNotifications(
admin.adminId,
`Docker cleanup for Server ${appName}`,
);
});
}
}
const pgs = await db.query.postgres.findMany({
with: {
project: true,
backups: {
with: {
destination: true,
@@ -61,18 +69,39 @@ export const initCronJobs = async () => {
for (const backup of pg.backups) {
const { schedule, backupId, enabled } = backup;
if (enabled) {
scheduleJob(backupId, schedule, async () => {
console.log(
`PG-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
);
runPostgresBackup(pg, backup);
});
try {
scheduleJob(backupId, schedule, async () => {
console.log(
`PG-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
);
runPostgresBackup(pg, backup);
});
await sendDatabaseBackupNotifications({
applicationName: pg.name,
projectName: pg.project.name,
databaseType: "postgres",
type: "success",
adminId: pg.project.adminId,
});
} catch (error) {
await sendDatabaseBackupNotifications({
applicationName: pg.name,
projectName: pg.project.name,
databaseType: "postgres",
type: "error",
// @ts-ignore
errorMessage: error?.message || "Error message not provided",
adminId: pg.project.adminId,
});
}
}
}
}
const mariadbs = await db.query.mariadb.findMany({
with: {
project: true,
backups: {
with: {
destination: true,
@@ -89,18 +118,38 @@ export const initCronJobs = async () => {
for (const backup of maria.backups) {
const { schedule, backupId, enabled } = backup;
if (enabled) {
scheduleJob(backupId, schedule, async () => {
console.log(
`MARIADB-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
);
await runMariadbBackup(maria, backup);
});
try {
scheduleJob(backupId, schedule, async () => {
console.log(
`MARIADB-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
);
await runMariadbBackup(maria, backup);
});
await sendDatabaseBackupNotifications({
applicationName: maria.name,
projectName: maria.project.name,
databaseType: "mariadb",
type: "success",
adminId: maria.project.adminId,
});
} catch (error) {
await sendDatabaseBackupNotifications({
applicationName: maria.name,
projectName: maria.project.name,
databaseType: "mariadb",
type: "error",
// @ts-ignore
errorMessage: error?.message || "Error message not provided",
adminId: maria.project.adminId,
});
}
}
}
}
const mongodbs = await db.query.mongo.findMany({
with: {
project: true,
backups: {
with: {
destination: true,
@@ -117,18 +166,38 @@ export const initCronJobs = async () => {
for (const backup of mongo.backups) {
const { schedule, backupId, enabled } = backup;
if (enabled) {
scheduleJob(backupId, schedule, async () => {
console.log(
`MONGO-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
);
await runMongoBackup(mongo, backup);
});
try {
scheduleJob(backupId, schedule, async () => {
console.log(
`MONGO-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
);
await runMongoBackup(mongo, backup);
});
await sendDatabaseBackupNotifications({
applicationName: mongo.name,
projectName: mongo.project.name,
databaseType: "mongodb",
type: "success",
adminId: mongo.project.adminId,
});
} catch (error) {
await sendDatabaseBackupNotifications({
applicationName: mongo.name,
projectName: mongo.project.name,
databaseType: "mongodb",
type: "error",
// @ts-ignore
errorMessage: error?.message || "Error message not provided",
adminId: mongo.project.adminId,
});
}
}
}
}
const mysqls = await db.query.mysql.findMany({
with: {
project: true,
backups: {
with: {
destination: true,
@@ -145,12 +214,31 @@ export const initCronJobs = async () => {
for (const backup of mysql.backups) {
const { schedule, backupId, enabled } = backup;
if (enabled) {
scheduleJob(backupId, schedule, async () => {
console.log(
`MYSQL-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
);
await runMySqlBackup(mysql, backup);
});
try {
scheduleJob(backupId, schedule, async () => {
console.log(
`MYSQL-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
);
await runMySqlBackup(mysql, backup);
});
await sendDatabaseBackupNotifications({
applicationName: mysql.name,
projectName: mysql.project.name,
databaseType: "mysql",
type: "success",
adminId: mysql.project.adminId,
});
} catch (error) {
await sendDatabaseBackupNotifications({
applicationName: mysql.name,
projectName: mysql.project.name,
databaseType: "mysql",
type: "error",
// @ts-ignore
errorMessage: error?.message || "Error message not provided",
adminId: mysql.project.adminId,
});
}
}
}
}

View File

@@ -48,6 +48,7 @@ Compose Type: ${composeType} ✅`;
writeStream.write(`\n${logBox}\n`);
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
await spawnAsync(
"docker",
[...command.split(" ")],
@@ -144,6 +145,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" ? "docker-compose.yml" : compose.composePath;
let command = "";
@@ -154,12 +159,6 @@ export const createCommand = (compose: ComposeNested) => {
command = `stack deploy -c ${path} ${appName} --prune`;
}
const customCommand = sanitizeCommand(compose.command);
if (customCommand) {
command = `${command} ${customCommand}`;
}
return command;
};

View File

@@ -211,21 +211,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) {

View File

@@ -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;
}
};

View File

@@ -28,7 +28,7 @@ export const sendBuildErrorNotifications = async ({
adminId,
}: Props) => {
const date = new Date();
const unixDate = ~~((Number(date)) / 1000);
const unixDate = ~~(Number(date) / 1000);
const notificationList = await db.query.notifications.findMany({
where: and(
eq(notifications.appBuildError, true),
@@ -60,45 +60,45 @@ export const sendBuildErrorNotifications = async ({
if (discord) {
await sendDiscordNotification(discord, {
title: "> `⚠️` - Build Failed",
title: "> `⚠️` Build Failed",
color: 0xed4245,
fields: [
{
name: "`🛠️`Project",
name: "`🛠️` Project",
value: projectName,
inline: true,
},
{
name: "`⚙️`Application",
name: "`⚙️` Application",
value: applicationName,
inline: true,
},
{
name: "`❔`Type",
name: "`❔` Type",
value: applicationType,
inline: true,
},
{
name: "`📅`Date",
name: "`📅` Date",
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: "`⌚`Time",
name: "`⌚` Time",
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: "`❓`Type",
name: "`❓`Type",
value: "Failed",
inline: true,
},
{
name: "`⚠️`Error Message",
name: "`⚠️` Error Message",
value: `\`\`\`${errorMessage}\`\`\``,
},
{
name: "`🧷`Build Link",
name: "`🧷` Build Link",
value: `[Click here to access build link](${buildLink})`,
},
],

View File

@@ -26,7 +26,7 @@ export const sendBuildSuccessNotifications = async ({
adminId,
}: Props) => {
const date = new Date();
const unixDate = ~~((Number(date)) / 1000);
const unixDate = ~~(Number(date) / 1000);
const notificationList = await db.query.notifications.findMany({
where: and(
eq(notifications.appDeploy, true),
@@ -58,41 +58,41 @@ export const sendBuildSuccessNotifications = async ({
if (discord) {
await sendDiscordNotification(discord, {
title: "> `✅` - Build Success",
title: "> `✅` Build Success",
color: 0x57f287,
fields: [
{
name: "`🛠️`Project",
name: "`🛠️` Project",
value: projectName,
inline: true,
},
{
name: "`⚙️`Application",
name: "`⚙️` Application",
value: applicationName,
inline: true,
},
{
name: "`❔`Application Type",
name: "`❔` Application Type",
value: applicationType,
inline: true,
},
{
name: "`📅`Date",
name: "`📅` Date",
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: "`⌚`Time",
name: "`⌚` Time",
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: "`❓`Type",
name: "`❓` Type",
value: "Successful",
inline: true,
},
{
name: "`🧷`Build Link",
name: "`🧷` Build Link",
value: `[Click here to access build link](${buildLink})`,
},
],

View File

@@ -26,7 +26,7 @@ export const sendDatabaseBackupNotifications = async ({
errorMessage?: string;
}) => {
const date = new Date();
const unixDate = ~~((Number(date)) / 1000);
const unixDate = ~~(Number(date) / 1000);
const notificationList = await db.query.notifications.findMany({
where: and(
eq(notifications.databaseBackup, true),
@@ -65,37 +65,37 @@ export const sendDatabaseBackupNotifications = async ({
await sendDiscordNotification(discord, {
title:
type === "success"
? "> `✅` - Database Backup Successful"
: "> `❌` - Database Backup Failed",
? "> `✅` Database Backup Successful"
: "> `❌` Database Backup Failed",
color: type === "success" ? 0x57f287 : 0xed4245,
fields: [
{
name: "`🛠️`Project",
name: "`🛠️` Project",
value: projectName,
inline: true,
},
{
name: "`⚙️`Application",
name: "`⚙️` Application",
value: applicationName,
inline: true,
},
{
name: "`❔`Database",
name: "`❔` Database",
value: databaseType,
inline: true,
},
{
name: "`📅`Date",
name: "`📅` Date",
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: "`⌚`Time",
name: "`⌚` Time",
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: "`❓`Type",
name: "`❓` Type",
value: type
.replace("error", "Failed")
.replace("success", "Successful"),
@@ -104,7 +104,7 @@ export const sendDatabaseBackupNotifications = async ({
...(type === "error" && errorMessage
? [
{
name: "`⚠️`Error Message",
name: "`⚠️` Error Message",
value: `\`\`\`${errorMessage}\`\`\``,
},
]

View File

@@ -15,7 +15,7 @@ export const sendDockerCleanupNotifications = async (
message = "Docker cleanup for dokploy",
) => {
const date = new Date();
const unixDate = ~~((Number(date)) / 1000);
const unixDate = ~~(Number(date) / 1000);
const notificationList = await db.query.notifications.findMany({
where: and(
eq(notifications.dockerCleanup, true),
@@ -46,26 +46,26 @@ export const sendDockerCleanupNotifications = async (
if (discord) {
await sendDiscordNotification(discord, {
title: "> `✅` - Docker Cleanup",
title: "> `✅` Docker Cleanup",
color: 0x57f287,
fields: [
{
name: "`📅`Date",
name: "`📅` Date",
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: "`⌚`Time",
name: "`⌚` Time",
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: "`❓`Type",
name: "`❓` Type",
value: "Successful",
inline: true,
},
{
name: "`📜`Message",
name: "`📜` Message",
value: `\`\`\`${message}\`\`\``,
},
],

View File

@@ -12,7 +12,7 @@ import {
export const sendDokployRestartNotifications = async () => {
const date = new Date();
const unixDate = ~~((Number(date)) / 1000);
const unixDate = ~~(Number(date) / 1000);
const notificationList = await db.query.notifications.findMany({
where: eq(notifications.dokployRestart, true),
with: {
@@ -35,21 +35,21 @@ export const sendDokployRestartNotifications = async () => {
if (discord) {
await sendDiscordNotification(discord, {
title: "> `✅` - Dokploy Server Restarted",
title: "> `✅` Dokploy Server Restarted",
color: 0x57f287,
fields: [
{
name: "`📅`Date",
name: "`📅` Date",
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: "`⌚`Time",
name: "`⌚` Time",
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: "`❓`Type",
name: "`❓` Type",
value: "Successful",
inline: true,
},

View File

@@ -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