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:
@@ -28,6 +28,7 @@ import {
|
||||
getCustomGitCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/git";
|
||||
import {
|
||||
authGithub,
|
||||
cloneGithubRepository,
|
||||
getGithubCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/github";
|
||||
@@ -40,8 +41,23 @@ import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { encodeBase64 } from "../utils/docker/utils";
|
||||
import { getDokployUrl } from "./admin";
|
||||
import { createDeployment, updateDeploymentStatus } from "./deployment";
|
||||
import {
|
||||
createDeployment,
|
||||
createDeploymentPreview,
|
||||
updateDeploymentStatus,
|
||||
} from "./deployment";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
import {
|
||||
findPreviewDeploymentById,
|
||||
updatePreviewDeployment,
|
||||
} from "./preview-deployment";
|
||||
import {
|
||||
createPreviewDeploymentComment,
|
||||
getIssueComment,
|
||||
issueCommentExists,
|
||||
updateIssueComment,
|
||||
} from "./github";
|
||||
import { type Domain, getDomainHost } from "./domain";
|
||||
export type Application = typeof applications.$inferSelect;
|
||||
|
||||
export const createApplication = async (
|
||||
@@ -100,6 +116,7 @@ export const findApplicationById = async (applicationId: string) => {
|
||||
github: true,
|
||||
bitbucket: true,
|
||||
server: true,
|
||||
previewDeployments: true,
|
||||
},
|
||||
});
|
||||
if (!application) {
|
||||
@@ -168,7 +185,10 @@ export const deployApplication = async ({
|
||||
|
||||
try {
|
||||
if (application.sourceType === "github") {
|
||||
await cloneGithubRepository(application, deployment.logPath);
|
||||
await cloneGithubRepository({
|
||||
...application,
|
||||
logPath: deployment.logPath,
|
||||
});
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "gitlab") {
|
||||
await cloneGitlabRepository(application, deployment.logPath);
|
||||
@@ -193,6 +213,7 @@ export const deployApplication = async ({
|
||||
applicationName: application.name,
|
||||
applicationType: "application",
|
||||
buildLink,
|
||||
adminId: application.project.adminId,
|
||||
});
|
||||
} catch (error) {
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
@@ -204,16 +225,9 @@ export const deployApplication = async ({
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error to build",
|
||||
buildLink,
|
||||
adminId: application.project.adminId,
|
||||
});
|
||||
|
||||
console.log(
|
||||
"Error on ",
|
||||
application.buildType,
|
||||
"/",
|
||||
application.sourceType,
|
||||
error,
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -282,7 +296,11 @@ export const deployRemoteApplication = async ({
|
||||
if (application.serverId) {
|
||||
let command = "set -e;";
|
||||
if (application.sourceType === "github") {
|
||||
command += await getGithubCloneCommand(application, deployment.logPath);
|
||||
command += await getGithubCloneCommand({
|
||||
...application,
|
||||
serverId: application.serverId,
|
||||
logPath: deployment.logPath,
|
||||
});
|
||||
} else if (application.sourceType === "gitlab") {
|
||||
command += await getGitlabCloneCommand(application, deployment.logPath);
|
||||
} else if (application.sourceType === "bitbucket") {
|
||||
@@ -314,6 +332,7 @@ export const deployRemoteApplication = async ({
|
||||
applicationName: application.name,
|
||||
applicationType: "application",
|
||||
buildLink,
|
||||
adminId: application.project.adminId,
|
||||
});
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
@@ -336,6 +355,7 @@ export const deployRemoteApplication = async ({
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error to build",
|
||||
buildLink,
|
||||
adminId: application.project.adminId,
|
||||
});
|
||||
|
||||
console.log(
|
||||
@@ -352,6 +372,225 @@ export const deployRemoteApplication = async ({
|
||||
return true;
|
||||
};
|
||||
|
||||
export const deployPreviewApplication = async ({
|
||||
applicationId,
|
||||
titleLog = "Preview Deployment",
|
||||
descriptionLog = "",
|
||||
previewDeploymentId,
|
||||
}: {
|
||||
applicationId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
previewDeploymentId: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
const deployment = await createDeploymentPreview({
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
previewDeploymentId: previewDeploymentId,
|
||||
});
|
||||
|
||||
const previewDeployment =
|
||||
await findPreviewDeploymentById(previewDeploymentId);
|
||||
|
||||
await updatePreviewDeployment(previewDeploymentId, {
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const previewDomain = getDomainHost(previewDeployment?.domain as Domain);
|
||||
const issueParams = {
|
||||
owner: application?.owner || "",
|
||||
repository: application?.repository || "",
|
||||
issue_number: previewDeployment.pullRequestNumber,
|
||||
comment_id: Number.parseInt(previewDeployment.pullRequestCommentId),
|
||||
githubId: application?.githubId || "",
|
||||
};
|
||||
try {
|
||||
const commentExists = await issueCommentExists({
|
||||
...issueParams,
|
||||
});
|
||||
if (!commentExists) {
|
||||
const result = await createPreviewDeploymentComment({
|
||||
...issueParams,
|
||||
previewDomain,
|
||||
appName: previewDeployment.appName,
|
||||
githubId: application?.githubId || "",
|
||||
previewDeploymentId,
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Pull request comment not found",
|
||||
});
|
||||
}
|
||||
|
||||
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
|
||||
}
|
||||
const buildingComment = getIssueComment(
|
||||
application.name,
|
||||
"running",
|
||||
previewDomain,
|
||||
);
|
||||
await updateIssueComment({
|
||||
...issueParams,
|
||||
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
||||
});
|
||||
application.appName = previewDeployment.appName;
|
||||
application.env = application.previewEnv;
|
||||
application.buildArgs = application.previewBuildArgs;
|
||||
|
||||
if (application.sourceType === "github") {
|
||||
await cloneGithubRepository({
|
||||
...application,
|
||||
appName: previewDeployment.appName,
|
||||
branch: previewDeployment.branch,
|
||||
logPath: deployment.logPath,
|
||||
});
|
||||
await buildApplication(application, deployment.logPath);
|
||||
}
|
||||
// 4eef09efc46009187d668cf1c25f768d0bde4f91
|
||||
const successComment = getIssueComment(
|
||||
application.name,
|
||||
"success",
|
||||
previewDomain,
|
||||
);
|
||||
await updateIssueComment({
|
||||
...issueParams,
|
||||
body: `### Dokploy Preview Deployment\n\n${successComment}`,
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updatePreviewDeployment(previewDeploymentId, {
|
||||
previewStatus: "done",
|
||||
});
|
||||
} catch (error) {
|
||||
const comment = getIssueComment(application.name, "error", previewDomain);
|
||||
await updateIssueComment({
|
||||
...issueParams,
|
||||
body: `### Dokploy Preview Deployment\n\n${comment}`,
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updatePreviewDeployment(previewDeploymentId, {
|
||||
previewStatus: "error",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const deployRemotePreviewApplication = async ({
|
||||
applicationId,
|
||||
titleLog = "Preview Deployment",
|
||||
descriptionLog = "",
|
||||
previewDeploymentId,
|
||||
}: {
|
||||
applicationId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
previewDeploymentId: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
const deployment = await createDeploymentPreview({
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
previewDeploymentId: previewDeploymentId,
|
||||
});
|
||||
|
||||
const previewDeployment =
|
||||
await findPreviewDeploymentById(previewDeploymentId);
|
||||
|
||||
await updatePreviewDeployment(previewDeploymentId, {
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const previewDomain = getDomainHost(previewDeployment?.domain as Domain);
|
||||
const issueParams = {
|
||||
owner: application?.owner || "",
|
||||
repository: application?.repository || "",
|
||||
issue_number: previewDeployment.pullRequestNumber,
|
||||
comment_id: Number.parseInt(previewDeployment.pullRequestCommentId),
|
||||
githubId: application?.githubId || "",
|
||||
};
|
||||
try {
|
||||
const commentExists = await issueCommentExists({
|
||||
...issueParams,
|
||||
});
|
||||
if (!commentExists) {
|
||||
const result = await createPreviewDeploymentComment({
|
||||
...issueParams,
|
||||
previewDomain,
|
||||
appName: previewDeployment.appName,
|
||||
githubId: application?.githubId || "",
|
||||
previewDeploymentId,
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Pull request comment not found",
|
||||
});
|
||||
}
|
||||
|
||||
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
|
||||
}
|
||||
const buildingComment = getIssueComment(
|
||||
application.name,
|
||||
"running",
|
||||
previewDomain,
|
||||
);
|
||||
await updateIssueComment({
|
||||
...issueParams,
|
||||
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
||||
});
|
||||
application.appName = previewDeployment.appName;
|
||||
application.env = application.previewEnv;
|
||||
application.buildArgs = application.previewBuildArgs;
|
||||
|
||||
if (application.serverId) {
|
||||
let command = "set -e;";
|
||||
if (application.sourceType === "github") {
|
||||
command += await getGithubCloneCommand({
|
||||
...application,
|
||||
serverId: application.serverId,
|
||||
logPath: deployment.logPath,
|
||||
});
|
||||
}
|
||||
|
||||
command += getBuildCommand(application, deployment.logPath);
|
||||
await execAsyncRemote(application.serverId, command);
|
||||
await mechanizeDockerContainer(application);
|
||||
}
|
||||
|
||||
const successComment = getIssueComment(
|
||||
application.name,
|
||||
"success",
|
||||
previewDomain,
|
||||
);
|
||||
await updateIssueComment({
|
||||
...issueParams,
|
||||
body: `### Dokploy Preview Deployment\n\n${successComment}`,
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updatePreviewDeployment(previewDeploymentId, {
|
||||
previewStatus: "done",
|
||||
});
|
||||
} catch (error) {
|
||||
const comment = getIssueComment(application.name, "error", previewDomain);
|
||||
await updateIssueComment({
|
||||
...issueParams,
|
||||
body: `### Dokploy Preview Deployment\n\n${comment}`,
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updatePreviewDeployment(previewDeploymentId, {
|
||||
previewStatus: "error",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const rebuildRemoteApplication = async ({
|
||||
applicationId,
|
||||
titleLog = "Rebuild deployment",
|
||||
|
||||
@@ -214,7 +214,11 @@ export const deployCompose = async ({
|
||||
|
||||
try {
|
||||
if (compose.sourceType === "github") {
|
||||
await cloneGithubRepository(compose, deployment.logPath, true);
|
||||
await cloneGithubRepository({
|
||||
...compose,
|
||||
logPath: deployment.logPath,
|
||||
type: "compose",
|
||||
});
|
||||
} else if (compose.sourceType === "gitlab") {
|
||||
await cloneGitlabRepository(compose, deployment.logPath, true);
|
||||
} else if (compose.sourceType === "bitbucket") {
|
||||
@@ -235,6 +239,7 @@ export const deployCompose = async ({
|
||||
applicationName: compose.name,
|
||||
applicationType: "compose",
|
||||
buildLink,
|
||||
adminId: compose.project.adminId,
|
||||
});
|
||||
} catch (error) {
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
@@ -248,6 +253,7 @@ export const deployCompose = async ({
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error to build",
|
||||
buildLink,
|
||||
adminId: compose.project.adminId,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
@@ -312,11 +318,12 @@ export const deployRemoteCompose = async ({
|
||||
let command = "set -e;";
|
||||
|
||||
if (compose.sourceType === "github") {
|
||||
command += await getGithubCloneCommand(
|
||||
compose,
|
||||
deployment.logPath,
|
||||
true,
|
||||
);
|
||||
command += await getGithubCloneCommand({
|
||||
...compose,
|
||||
logPath: deployment.logPath,
|
||||
type: "compose",
|
||||
serverId: compose.serverId,
|
||||
});
|
||||
} else if (compose.sourceType === "gitlab") {
|
||||
command += await getGitlabCloneCommand(
|
||||
compose,
|
||||
@@ -353,6 +360,7 @@ export const deployRemoteCompose = async ({
|
||||
applicationName: compose.name,
|
||||
applicationType: "compose",
|
||||
buildLink,
|
||||
adminId: compose.project.adminId,
|
||||
});
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
@@ -376,6 +384,7 @@ export const deployRemoteCompose = async ({
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error to build",
|
||||
buildLink,
|
||||
adminId: compose.project.adminId,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
@@ -459,6 +468,36 @@ export const removeCompose = async (compose: Compose) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
export const startCompose = async (composeId: string) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
try {
|
||||
const { COMPOSE_PATH } = paths(!!compose.serverId);
|
||||
if (compose.composeType === "docker-compose") {
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(
|
||||
compose.serverId,
|
||||
`cd ${join(COMPOSE_PATH, compose.appName, "code")} && docker compose -p ${compose.appName} up -d`,
|
||||
);
|
||||
} else {
|
||||
await execAsync(`docker compose -p ${compose.appName} up -d`, {
|
||||
cwd: join(COMPOSE_PATH, compose.appName, "code"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "done",
|
||||
});
|
||||
} catch (error) {
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "idle",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const stopCompose = async (composeId: string) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
try {
|
||||
|
||||
@@ -5,13 +5,14 @@ import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
type apiCreateDeployment,
|
||||
type apiCreateDeploymentCompose,
|
||||
type apiCreateDeploymentPreview,
|
||||
type apiCreateDeploymentServer,
|
||||
deployments,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { format } from "date-fns";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { and, desc, eq, isNull } from "drizzle-orm";
|
||||
import {
|
||||
type Application,
|
||||
findApplicationById,
|
||||
@@ -21,6 +22,11 @@ import { type Compose, findComposeById, updateCompose } from "./compose";
|
||||
import { type Server, findServerById } from "./server";
|
||||
|
||||
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
import {
|
||||
findPreviewDeploymentById,
|
||||
type PreviewDeployment,
|
||||
updatePreviewDeployment,
|
||||
} from "./preview-deployment";
|
||||
|
||||
export type Deployment = typeof deployments.$inferSelect;
|
||||
|
||||
@@ -101,6 +107,74 @@ export const createDeployment = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const createDeploymentPreview = async (
|
||||
deployment: Omit<
|
||||
typeof apiCreateDeploymentPreview._type,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
>,
|
||||
) => {
|
||||
const previewDeployment = await findPreviewDeploymentById(
|
||||
deployment.previewDeploymentId,
|
||||
);
|
||||
try {
|
||||
await removeLastTenPreviewDeploymenById(
|
||||
deployment.previewDeploymentId,
|
||||
previewDeployment?.application?.serverId,
|
||||
);
|
||||
|
||||
const appName = `${previewDeployment.appName}`;
|
||||
const { LOGS_PATH } = paths(!!previewDeployment?.application?.serverId);
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||
const fileName = `${appName}-${formattedDateTime}.log`;
|
||||
const logFilePath = path.join(LOGS_PATH, appName, fileName);
|
||||
|
||||
if (previewDeployment?.application?.serverId) {
|
||||
const server = await findServerById(
|
||||
previewDeployment?.application?.serverId,
|
||||
);
|
||||
|
||||
const command = `
|
||||
mkdir -p ${LOGS_PATH}/${appName};
|
||||
echo "Initializing deployment" >> ${logFilePath};
|
||||
`;
|
||||
|
||||
await execAsyncRemote(server.serverId, command);
|
||||
} else {
|
||||
await fsPromises.mkdir(path.join(LOGS_PATH, appName), {
|
||||
recursive: true,
|
||||
});
|
||||
await fsPromises.writeFile(logFilePath, "Initializing deployment");
|
||||
}
|
||||
|
||||
const deploymentCreate = await db
|
||||
.insert(deployments)
|
||||
.values({
|
||||
title: deployment.title || "Deployment",
|
||||
status: "running",
|
||||
logPath: logFilePath,
|
||||
description: deployment.description || "",
|
||||
previewDeploymentId: deployment.previewDeploymentId,
|
||||
})
|
||||
.returning();
|
||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the deployment",
|
||||
});
|
||||
}
|
||||
return deploymentCreate[0];
|
||||
} catch (error) {
|
||||
await updatePreviewDeployment(deployment.previewDeploymentId, {
|
||||
previewStatus: "error",
|
||||
});
|
||||
console.log(error);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the deployment",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const createDeploymentCompose = async (
|
||||
deployment: Omit<
|
||||
typeof apiCreateDeploymentCompose._type,
|
||||
@@ -257,6 +331,41 @@ const removeLastTenComposeDeployments = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const removeLastTenPreviewDeploymenById = async (
|
||||
previewDeploymentId: string,
|
||||
serverId: string | null,
|
||||
) => {
|
||||
const deploymentList = await db.query.deployments.findMany({
|
||||
where: eq(deployments.previewDeploymentId, previewDeploymentId),
|
||||
orderBy: desc(deployments.createdAt),
|
||||
});
|
||||
|
||||
if (deploymentList.length > 10) {
|
||||
const deploymentsToDelete = deploymentList.slice(10);
|
||||
if (serverId) {
|
||||
let command = "";
|
||||
for (const oldDeployment of deploymentsToDelete) {
|
||||
const logPath = path.join(oldDeployment.logPath);
|
||||
|
||||
command += `
|
||||
rm -rf ${logPath};
|
||||
`;
|
||||
await removeDeployment(oldDeployment.deploymentId);
|
||||
}
|
||||
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
for (const oldDeployment of deploymentsToDelete) {
|
||||
const logPath = path.join(oldDeployment.logPath);
|
||||
if (existsSync(logPath)) {
|
||||
await fsPromises.unlink(logPath);
|
||||
}
|
||||
await removeDeployment(oldDeployment.deploymentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const removeDeployments = async (application: Application) => {
|
||||
const { appName, applicationId } = application;
|
||||
const { LOGS_PATH } = paths(!!application.serverId);
|
||||
@@ -269,6 +378,30 @@ export const removeDeployments = async (application: Application) => {
|
||||
await removeDeploymentsByApplicationId(applicationId);
|
||||
};
|
||||
|
||||
export const removeDeploymentsByPreviewDeploymentId = async (
|
||||
previewDeployment: PreviewDeployment,
|
||||
serverId: string | null,
|
||||
) => {
|
||||
const { appName } = previewDeployment;
|
||||
const { LOGS_PATH } = paths(!!serverId);
|
||||
const logsPath = path.join(LOGS_PATH, appName);
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, `rm -rf ${logsPath}`);
|
||||
} else {
|
||||
await removeDirectoryIfExistsContent(logsPath);
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(deployments)
|
||||
.where(
|
||||
eq(
|
||||
deployments.previewDeploymentId,
|
||||
previewDeployment.previewDeploymentId,
|
||||
),
|
||||
)
|
||||
.returning();
|
||||
};
|
||||
|
||||
export const removeDeploymentsByComposeId = async (compose: Compose) => {
|
||||
const { appName } = compose;
|
||||
const { LOGS_PATH } = paths(!!compose.serverId);
|
||||
|
||||
@@ -110,7 +110,7 @@ export const getContainersByAppNameMatch = async (
|
||||
const command =
|
||||
appType === "docker-compose"
|
||||
? `${cmd} --filter='label=com.docker.compose.project=${appName}'`
|
||||
: `${cmd} | grep ${appName}`;
|
||||
: `${cmd} | grep '^.*Name: ${appName}'`;
|
||||
if (serverId) {
|
||||
const { stdout, stderr } = await execAsyncRemote(serverId, command);
|
||||
|
||||
|
||||
@@ -134,3 +134,7 @@ export const removeDomainById = async (domainId: string) => {
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const getDomainHost = (domain: Domain) => {
|
||||
return `${domain.https ? "https" : "http"}://${domain.host}`;
|
||||
};
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { authGithub } from "../utils/providers/github";
|
||||
import { updatePreviewDeployment } from "./preview-deployment";
|
||||
|
||||
export type Github = typeof github.$inferSelect;
|
||||
export const createGithub = async (
|
||||
@@ -72,3 +74,119 @@ export const updateGithub = async (
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
};
|
||||
|
||||
export const getIssueComment = (
|
||||
appName: string,
|
||||
status: "success" | "error" | "running" | "initializing",
|
||||
previewDomain: string,
|
||||
) => {
|
||||
let statusMessage = "";
|
||||
if (status === "success") {
|
||||
statusMessage = "✅ Done";
|
||||
} else if (status === "error") {
|
||||
statusMessage = "❌ Failed";
|
||||
} else if (status === "initializing") {
|
||||
statusMessage = "🔄 Building";
|
||||
} else {
|
||||
statusMessage = "🔄 Building";
|
||||
}
|
||||
const finished = `
|
||||
| Name | Status | Preview | Updated (UTC) |
|
||||
|------------|--------------|-------------------------------------|-----------------------|
|
||||
| ${appName} | ${statusMessage} | [Preview URL](${previewDomain}) | ${new Date().toISOString()} |
|
||||
`;
|
||||
|
||||
return finished;
|
||||
};
|
||||
interface CommentExists {
|
||||
owner: string;
|
||||
repository: string;
|
||||
comment_id: number;
|
||||
githubId: string;
|
||||
}
|
||||
export const issueCommentExists = async ({
|
||||
owner,
|
||||
repository,
|
||||
comment_id,
|
||||
githubId,
|
||||
}: CommentExists) => {
|
||||
const github = await findGithubById(githubId);
|
||||
const octokit = authGithub(github);
|
||||
try {
|
||||
await octokit.rest.issues.getComment({
|
||||
owner: owner || "",
|
||||
repo: repository || "",
|
||||
comment_id: comment_id,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
interface Comment {
|
||||
owner: string;
|
||||
repository: string;
|
||||
issue_number: string;
|
||||
body: string;
|
||||
comment_id: number;
|
||||
githubId: string;
|
||||
}
|
||||
export const updateIssueComment = async ({
|
||||
owner,
|
||||
repository,
|
||||
issue_number,
|
||||
body,
|
||||
comment_id,
|
||||
githubId,
|
||||
}: Comment) => {
|
||||
const github = await findGithubById(githubId);
|
||||
const octokit = authGithub(github);
|
||||
|
||||
await octokit.rest.issues.updateComment({
|
||||
owner: owner || "",
|
||||
repo: repository || "",
|
||||
issue_number: issue_number,
|
||||
body,
|
||||
comment_id: comment_id,
|
||||
});
|
||||
};
|
||||
|
||||
interface CommentCreate {
|
||||
appName: string;
|
||||
owner: string;
|
||||
repository: string;
|
||||
issue_number: string;
|
||||
previewDomain: string;
|
||||
githubId: string;
|
||||
previewDeploymentId: string;
|
||||
}
|
||||
|
||||
export const createPreviewDeploymentComment = async ({
|
||||
owner,
|
||||
repository,
|
||||
issue_number,
|
||||
previewDomain,
|
||||
appName,
|
||||
githubId,
|
||||
previewDeploymentId,
|
||||
}: CommentCreate) => {
|
||||
const github = await findGithubById(githubId);
|
||||
const octokit = authGithub(github);
|
||||
|
||||
const runningComment = getIssueComment(
|
||||
appName,
|
||||
"initializing",
|
||||
previewDomain,
|
||||
);
|
||||
|
||||
const issue = await octokit.rest.issues.createComment({
|
||||
owner: owner || "",
|
||||
repo: repository || "",
|
||||
issue_number: Number.parseInt(issue_number),
|
||||
body: `### Dokploy Preview Deployment\n\n${runningComment}`,
|
||||
});
|
||||
|
||||
return await updatePreviewDeployment(previewDeploymentId, {
|
||||
pullRequestCommentId: `${issue.data.id}`,
|
||||
}).then((response) => response[0]);
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ export type Mongo = typeof mongo.$inferSelect;
|
||||
|
||||
export const createMongo = async (input: typeof apiCreateMongo._type) => {
|
||||
input.appName =
|
||||
`${input.appName}-${generatePassword(6)}` || generateAppName("postgres");
|
||||
`${input.appName}-${generatePassword(6)}` || generateAppName("mongo");
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
|
||||
@@ -72,12 +72,12 @@ export const findMongoById = async (mongoId: string) => {
|
||||
|
||||
export const updateMongoById = async (
|
||||
mongoId: string,
|
||||
postgresData: Partial<Mongo>,
|
||||
mongoData: Partial<Mongo>,
|
||||
) => {
|
||||
const result = await db
|
||||
.update(mongo)
|
||||
.set({
|
||||
...postgresData,
|
||||
...mongoData,
|
||||
})
|
||||
.where(eq(mongo.mongoId, mongoId))
|
||||
.returning();
|
||||
|
||||
283
packages/server/src/services/preview-deployment.ts
Normal file
283
packages/server/src/services/preview-deployment.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
type apiCreatePreviewDeployment,
|
||||
deployments,
|
||||
previewDeployments,
|
||||
} from "@dokploy/server/db/schema";
|
||||
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 { manageDomain } from "../utils/traefik/domain";
|
||||
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";
|
||||
|
||||
export type PreviewDeployment = typeof previewDeployments.$inferSelect;
|
||||
|
||||
export const findPreviewDeploymentById = async (
|
||||
previewDeploymentId: string,
|
||||
) => {
|
||||
const application = await db.query.previewDeployments.findFirst({
|
||||
where: eq(previewDeployments.previewDeploymentId, previewDeploymentId),
|
||||
with: {
|
||||
domain: true,
|
||||
application: {
|
||||
with: {
|
||||
server: true,
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!application) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Preview Deployment not found",
|
||||
});
|
||||
}
|
||||
return application;
|
||||
};
|
||||
|
||||
export const findApplicationByPreview = async (applicationId: string) => {
|
||||
const application = await db.query.applications.findFirst({
|
||||
with: {
|
||||
previewDeployments: {
|
||||
where: eq(previewDeployments.applicationId, applicationId),
|
||||
},
|
||||
project: true,
|
||||
domains: true,
|
||||
deployments: true,
|
||||
mounts: true,
|
||||
redirects: true,
|
||||
security: true,
|
||||
ports: true,
|
||||
registry: true,
|
||||
gitlab: true,
|
||||
github: true,
|
||||
bitbucket: true,
|
||||
server: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!application) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Applicationnot found",
|
||||
});
|
||||
}
|
||||
return application;
|
||||
};
|
||||
|
||||
export const removePreviewDeployment = async (previewDeploymentId: string) => {
|
||||
try {
|
||||
const application = await findApplicationByPreview(previewDeploymentId);
|
||||
const previewDeployment =
|
||||
await findPreviewDeploymentById(previewDeploymentId);
|
||||
|
||||
const deployment = await db
|
||||
.delete(previewDeployments)
|
||||
.where(eq(previewDeployments.previewDeploymentId, previewDeploymentId))
|
||||
.returning();
|
||||
|
||||
application.appName = previewDeployment.appName;
|
||||
const cleanupOperations = [
|
||||
async () =>
|
||||
await removeDeploymentsByPreviewDeploymentId(
|
||||
previewDeployment,
|
||||
application.serverId,
|
||||
),
|
||||
async () =>
|
||||
await removeDirectoryCode(application.appName, application.serverId),
|
||||
async () =>
|
||||
await removeTraefikConfig(application.appName, application.serverId),
|
||||
async () =>
|
||||
await removeService(application?.appName, application.serverId),
|
||||
];
|
||||
for (const operation of cleanupOperations) {
|
||||
try {
|
||||
await operation();
|
||||
} catch (error) {}
|
||||
}
|
||||
return deployment[0];
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to delete this preview deployment",
|
||||
});
|
||||
}
|
||||
};
|
||||
// testing-tesoitnmg-ddq0ul-preview-ihl44o
|
||||
export const updatePreviewDeployment = async (
|
||||
previewDeploymentId: string,
|
||||
previewDeploymentData: Partial<PreviewDeployment>,
|
||||
) => {
|
||||
const application = await db
|
||||
.update(previewDeployments)
|
||||
.set({
|
||||
...previewDeploymentData,
|
||||
})
|
||||
.where(eq(previewDeployments.previewDeploymentId, previewDeploymentId))
|
||||
.returning();
|
||||
|
||||
return application;
|
||||
};
|
||||
|
||||
export const findPreviewDeploymentsByApplicationId = async (
|
||||
applicationId: string,
|
||||
) => {
|
||||
const deploymentsList = await db.query.previewDeployments.findMany({
|
||||
where: eq(previewDeployments.applicationId, applicationId),
|
||||
orderBy: desc(previewDeployments.createdAt),
|
||||
with: {
|
||||
deployments: {
|
||||
orderBy: desc(deployments.createdAt),
|
||||
},
|
||||
domain: true,
|
||||
},
|
||||
});
|
||||
return deploymentsList;
|
||||
};
|
||||
|
||||
export const createPreviewDeployment = async (
|
||||
schema: typeof apiCreatePreviewDeployment._type,
|
||||
) => {
|
||||
const application = await findApplicationById(schema.applicationId);
|
||||
const appName = `preview-${application.appName}-${generatePassword(6)}`;
|
||||
|
||||
const generateDomain = await generateWildcardDomain(
|
||||
application.previewWildcard || "*.traefik.me",
|
||||
appName,
|
||||
application.server?.ipAddress || "",
|
||||
application.project.adminId,
|
||||
);
|
||||
|
||||
const octokit = authGithub(application?.github as Github);
|
||||
|
||||
const runningComment = getIssueComment(
|
||||
application.name,
|
||||
"initializing",
|
||||
generateDomain,
|
||||
);
|
||||
|
||||
const issue = await octokit.rest.issues.createComment({
|
||||
owner: application?.owner || "",
|
||||
repo: application?.repository || "",
|
||||
issue_number: Number.parseInt(schema.pullRequestNumber),
|
||||
body: `### Dokploy Preview Deployment\n\n${runningComment}`,
|
||||
});
|
||||
|
||||
const previewDeployment = await db
|
||||
.insert(previewDeployments)
|
||||
.values({
|
||||
...schema,
|
||||
appName: appName,
|
||||
pullRequestCommentId: `${issue.data.id}`,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!previewDeployment) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the preview deployment",
|
||||
});
|
||||
}
|
||||
|
||||
const newDomain = await createDomain({
|
||||
host: generateDomain,
|
||||
path: application.previewPath,
|
||||
port: application.previewPort,
|
||||
https: application.previewHttps,
|
||||
certificateType: application.previewCertificateType,
|
||||
domainType: "preview",
|
||||
previewDeploymentId: previewDeployment.previewDeploymentId,
|
||||
});
|
||||
|
||||
application.appName = appName;
|
||||
|
||||
await manageDomain(application, newDomain);
|
||||
|
||||
await db
|
||||
.update(previewDeployments)
|
||||
.set({
|
||||
domainId: newDomain.domainId,
|
||||
})
|
||||
.where(
|
||||
eq(
|
||||
previewDeployments.previewDeploymentId,
|
||||
previewDeployment.previewDeploymentId,
|
||||
),
|
||||
);
|
||||
|
||||
return previewDeployment;
|
||||
};
|
||||
|
||||
export const findPreviewDeploymentsByPullRequestId = async (
|
||||
pullRequestId: string,
|
||||
) => {
|
||||
const previewDeploymentResult = await db.query.previewDeployments.findMany({
|
||||
where: eq(previewDeployments.pullRequestId, pullRequestId),
|
||||
});
|
||||
|
||||
return previewDeploymentResult;
|
||||
};
|
||||
|
||||
export const findPreviewDeploymentByApplicationId = async (
|
||||
applicationId: string,
|
||||
pullRequestId: string,
|
||||
) => {
|
||||
const previewDeploymentResult = await db.query.previewDeployments.findFirst({
|
||||
where: and(
|
||||
eq(previewDeployments.applicationId, applicationId),
|
||||
eq(previewDeployments.pullRequestId, pullRequestId),
|
||||
),
|
||||
});
|
||||
|
||||
return previewDeploymentResult;
|
||||
};
|
||||
|
||||
const generateWildcardDomain = async (
|
||||
baseDomain: string,
|
||||
appName: string,
|
||||
serverIp: string,
|
||||
adminId: string,
|
||||
): Promise<string> => {
|
||||
if (!baseDomain.startsWith("*.")) {
|
||||
throw new Error('The base domain must start with "*."');
|
||||
}
|
||||
const hash = `${appName}`;
|
||||
if (baseDomain.includes("traefik.me")) {
|
||||
let ip = "";
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
ip = "127.0.0.1";
|
||||
}
|
||||
|
||||
if (serverIp) {
|
||||
ip = serverIp;
|
||||
}
|
||||
|
||||
if (!ip) {
|
||||
const admin = await findAdminById(adminId);
|
||||
ip = admin?.serverIp || "";
|
||||
}
|
||||
|
||||
const slugIp = ip.replaceAll(".", "-");
|
||||
return baseDomain.replace(
|
||||
"*",
|
||||
`${hash}${slugIp === "" ? "" : `-${slugIp}`}`,
|
||||
);
|
||||
}
|
||||
|
||||
return baseDomain.replace("*", hash);
|
||||
};
|
||||
Reference in New Issue
Block a user