mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat: add preview deployments #379
This commit is contained in:
285
packages/server/src/services/preview-deployment.ts
Normal file
285
packages/server/src/services/preview-deployment.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
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;
|
||||
|
||||
console.log(application);
|
||||
|
||||
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