mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
refactor(server): add support for multi server
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect, useState } from "react";
|
||||
export const DockerLogs = dynamic(
|
||||
@@ -34,7 +35,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
||||
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
{
|
||||
appName,
|
||||
serverId,
|
||||
@@ -64,7 +65,14 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
||||
<Label>Select a container to view logs</Label>
|
||||
<Select onValueChange={setContainerId} value={containerId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a container" />
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
|
||||
<span>Loading...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
) : (
|
||||
<SelectValue placeholder="Select a container" />
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
import { Loader, Loader2 } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect, useState } from "react";
|
||||
export const DockerLogs = dynamic(
|
||||
@@ -39,7 +40,7 @@ export const ShowDockerLogsCompose = ({
|
||||
appType,
|
||||
serverId,
|
||||
}: Props) => {
|
||||
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
{
|
||||
appName,
|
||||
appType,
|
||||
@@ -70,7 +71,14 @@ export const ShowDockerLogsCompose = ({
|
||||
<Label>Select a container to view logs</Label>
|
||||
<Select onValueChange={setContainerId} value={containerId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a container" />
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
|
||||
<span>Loading...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
) : (
|
||||
<SelectValue placeholder="Select a container" />
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
|
||||
@@ -20,6 +20,7 @@ import { api } from "@/utils/api";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { DockerMonitoring } from "../../monitoring/docker/show";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
interface Props {
|
||||
appName: string;
|
||||
@@ -32,7 +33,7 @@ export const ShowMonitoringCompose = ({
|
||||
appType = "stack",
|
||||
serverId,
|
||||
}: Props) => {
|
||||
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
{
|
||||
appName: appName,
|
||||
appType,
|
||||
@@ -49,7 +50,7 @@ export const ShowMonitoringCompose = ({
|
||||
|
||||
const [containerId, setContainerId] = useState<string | undefined>();
|
||||
|
||||
const { mutateAsync: restart, isLoading } =
|
||||
const { mutateAsync: restart, isLoading: isRestarting } =
|
||||
api.docker.restartContainer.useMutation();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -80,7 +81,14 @@ export const ShowMonitoringCompose = ({
|
||||
value={containerAppName}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a container" />
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
|
||||
<span>Loading...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
) : (
|
||||
<SelectValue placeholder="Select a container" />
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
@@ -98,7 +106,7 @@ export const ShowMonitoringCompose = ({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
isLoading={isRestarting}
|
||||
onClick={async () => {
|
||||
if (!containerId) return;
|
||||
toast.success(`Restarting container ${containerAppName}`);
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -38,7 +39,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
|
||||
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
{
|
||||
appName,
|
||||
serverId,
|
||||
@@ -67,7 +68,14 @@ export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
|
||||
<Label>Select a container to view logs</Label>
|
||||
<Select onValueChange={setContainerId} value={containerId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a container" />
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
|
||||
<span>Loading...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
) : (
|
||||
<SelectValue placeholder="Select a container" />
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -38,7 +39,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ShowModalLogs = ({ appName, children }: Props) => {
|
||||
const { data } = api.docker.getContainersByAppLabel.useQuery(
|
||||
const { data, isLoading } = api.docker.getContainersByAppLabel.useQuery(
|
||||
{
|
||||
appName,
|
||||
},
|
||||
@@ -72,7 +73,14 @@ export const ShowModalLogs = ({ appName, children }: Props) => {
|
||||
<Label>Select a container to view logs</Label>
|
||||
<Select onValueChange={setContainerId} value={containerId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a container" />
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
|
||||
<span>Loading...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
) : (
|
||||
<SelectValue placeholder="Select a container" />
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
type DeploymentJob,
|
||||
cleanQueuesByApplication,
|
||||
} from "@/server/queues/deployments-queue";
|
||||
import { enqueueDeploymentJob, myQueue } from "@/server/queues/queueSetup";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import {
|
||||
removeService,
|
||||
startService,
|
||||
@@ -134,12 +134,19 @@ export const applicationRouter = createTRPCRouter({
|
||||
.returning();
|
||||
|
||||
const cleanupOperations = [
|
||||
async () => deleteAllMiddlewares(application),
|
||||
async () => await deleteAllMiddlewares(application),
|
||||
async () => await removeDeployments(application),
|
||||
async () => await removeDirectoryCode(application?.appName),
|
||||
async () => await removeMonitoringDirectory(application?.appName),
|
||||
async () => await removeTraefikConfig(application?.appName),
|
||||
async () => await removeService(application?.appName),
|
||||
async () =>
|
||||
await removeDirectoryCode(application.appName, application.serverId),
|
||||
async () =>
|
||||
await removeMonitoringDirectory(
|
||||
application.appName,
|
||||
application.serverId,
|
||||
),
|
||||
async () =>
|
||||
await removeTraefikConfig(application.appName, application.serverId),
|
||||
async () =>
|
||||
await removeService(application?.appName, application.serverId),
|
||||
];
|
||||
|
||||
for (const operation of cleanupOperations) {
|
||||
@@ -327,7 +334,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
deploy: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
// const application = await findApplicationById(input.applicationId);
|
||||
const jobData: DeploymentJob = {
|
||||
applicationId: input.applicationId,
|
||||
titleLog: "Manual deployment",
|
||||
@@ -335,10 +342,18 @@ export const applicationRouter = createTRPCRouter({
|
||||
type: "deploy",
|
||||
applicationType: "application",
|
||||
};
|
||||
if (!application.serverId) {
|
||||
} else {
|
||||
await enqueueDeploymentJob(application.serverId, jobData);
|
||||
}
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
// if (!application.serverId) {
|
||||
// } else {
|
||||
// await enqueueDeploymentJob(application.serverId, jobData);
|
||||
// }
|
||||
}),
|
||||
|
||||
cleanQueues: protectedProcedure
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
type DeploymentJob,
|
||||
cleanQueuesByCompose,
|
||||
} from "@/server/queues/deployments-queue";
|
||||
import { enqueueDeploymentJob, myQueue } from "@/server/queues/queueSetup";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import { createCommand } from "@/server/utils/builders/compose";
|
||||
import {
|
||||
randomizeComposeFile,
|
||||
@@ -176,14 +176,7 @@ export const composeRouter = createTRPCRouter({
|
||||
// applicationType: "compose",
|
||||
// descriptionLog: "",
|
||||
// };
|
||||
// await myQueue.add(
|
||||
// "deployments",
|
||||
// { ...jobData },
|
||||
// {
|
||||
// removeOnComplete: true,
|
||||
// removeOnFail: true,
|
||||
// },
|
||||
// );
|
||||
|
||||
const compose = await findComposeById(input.composeId);
|
||||
const jobData: DeploymentJob = {
|
||||
composeId: input.composeId,
|
||||
@@ -192,10 +185,18 @@ export const composeRouter = createTRPCRouter({
|
||||
applicationType: "compose",
|
||||
descriptionLog: "",
|
||||
};
|
||||
if (!compose.serverId) {
|
||||
} else {
|
||||
await enqueueDeploymentJob(compose.serverId, jobData);
|
||||
}
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
// if (!compose.serverId) {
|
||||
// } else {
|
||||
// await enqueueDeploymentJob(compose.serverId, jobData);
|
||||
// }
|
||||
}),
|
||||
redeploy: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
} from "@/server/utils/providers/gitlab";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
import { executeCommand } from "@/server/utils/servers/command";
|
||||
import { execAsyncRemote } from "@/server/utils/process/execAsync";
|
||||
export type Application = typeof applications.$inferSelect;
|
||||
|
||||
export const createApplication = async (
|
||||
@@ -164,7 +165,7 @@ export const deployApplication = async ({
|
||||
|
||||
try {
|
||||
if (application.serverId) {
|
||||
let command = "set -e";
|
||||
let command = "set -e;";
|
||||
if (application.sourceType === "github") {
|
||||
command += await getGithubCloneCommand(application, deployment.logPath);
|
||||
} else if (application.sourceType === "gitlab") {
|
||||
@@ -186,8 +187,7 @@ export const deployApplication = async ({
|
||||
if (application.sourceType !== "docker") {
|
||||
command += getBuildCommand(application, deployment.logPath);
|
||||
}
|
||||
|
||||
await executeCommand(application.serverId, command);
|
||||
await execAsyncRemote(application.serverId, command);
|
||||
await mechanizeDockerContainer(application);
|
||||
} else {
|
||||
if (application.sourceType === "github") {
|
||||
@@ -209,8 +209,6 @@ export const deployApplication = async ({
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Command", "Finish");
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateApplicationStatus(applicationId, "done");
|
||||
|
||||
@@ -263,18 +261,31 @@ export const rebuildApplication = async ({
|
||||
});
|
||||
|
||||
try {
|
||||
if (application.sourceType === "github") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "gitlab") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "bitbucket") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "docker") {
|
||||
await buildDocker(application, deployment.logPath);
|
||||
} else if (application.sourceType === "git") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "drop") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
if (application.serverId) {
|
||||
let command = "set -e;";
|
||||
if (application.sourceType === "docker") {
|
||||
command += await buildRemoteDocker(application, deployment.logPath);
|
||||
}
|
||||
|
||||
if (application.sourceType !== "docker") {
|
||||
command += getBuildCommand(application, deployment.logPath);
|
||||
}
|
||||
await execAsyncRemote(application.serverId, command);
|
||||
await mechanizeDockerContainer(application);
|
||||
} else {
|
||||
if (application.sourceType === "github") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "gitlab") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "bitbucket") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "docker") {
|
||||
await buildDocker(application, deployment.logPath);
|
||||
} else if (application.sourceType === "git") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "drop") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
}
|
||||
}
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateApplicationStatus(applicationId, "done");
|
||||
|
||||
@@ -241,20 +241,6 @@ export const deployCompose = async ({
|
||||
} else if (compose.sourceType === "raw") {
|
||||
command += getCreateComposeFileCommand(compose);
|
||||
}
|
||||
|
||||
// Promise.resolve()
|
||||
// .then(() => {
|
||||
// return execAsyncRemote(compose.serverId, command);
|
||||
// })
|
||||
// .then(() => {
|
||||
// return getBuildComposeCommand(compose, deployment.logPath);
|
||||
// })
|
||||
// .catch((err) => {
|
||||
// throw err;
|
||||
// })
|
||||
// .then(() => {
|
||||
// console.log(" ---- done ----");
|
||||
// });
|
||||
async function* sequentialSteps() {
|
||||
yield execAsyncRemote(compose.serverId, command);
|
||||
yield getBuildComposeCommand(compose, deployment.logPath);
|
||||
@@ -262,9 +248,6 @@ export const deployCompose = async ({
|
||||
|
||||
const steps = sequentialSteps();
|
||||
for await (const step of steps) {
|
||||
if (step.stderr) {
|
||||
console.log(step.stderr);
|
||||
}
|
||||
step;
|
||||
}
|
||||
|
||||
@@ -330,7 +313,12 @@ export const rebuildCompose = async ({
|
||||
});
|
||||
|
||||
try {
|
||||
await buildCompose(compose, deployment.logPath);
|
||||
if (compose.serverId) {
|
||||
await getBuildComposeCommand(compose, deployment.logPath);
|
||||
} else {
|
||||
await buildCompose(compose, deployment.logPath);
|
||||
}
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "done",
|
||||
@@ -351,13 +339,24 @@ export const removeCompose = async (compose: Compose) => {
|
||||
const projectPath = join(COMPOSE_PATH, compose.appName);
|
||||
|
||||
if (compose.composeType === "stack") {
|
||||
await execAsync(`docker stack rm ${compose.appName}`, {
|
||||
const command = `cd ${projectPath} && docker stack rm ${compose.appName} && rm -rf ${projectPath}`;
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
await execAsync(command, {
|
||||
cwd: projectPath,
|
||||
});
|
||||
} else {
|
||||
await execAsync(`docker compose -p ${compose.appName} down`, {
|
||||
cwd: projectPath,
|
||||
});
|
||||
const command = `cd ${projectPath} && docker compose -p ${compose.appName} down && rm -rf ${projectPath}`;
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
} else {
|
||||
await execAsync(command, {
|
||||
cwd: projectPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -370,9 +369,16 @@ export const stopCompose = async (composeId: string) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
try {
|
||||
if (compose.composeType === "docker-compose") {
|
||||
await execAsync(`docker compose -p ${compose.appName} stop`, {
|
||||
cwd: join(COMPOSE_PATH, compose.appName),
|
||||
});
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(
|
||||
compose.serverId,
|
||||
`cd ${join(COMPOSE_PATH, compose.appName)} && docker compose -p ${compose.appName} stop`,
|
||||
);
|
||||
} else {
|
||||
await execAsync(`docker compose -p ${compose.appName} stop`, {
|
||||
cwd: join(COMPOSE_PATH, compose.appName),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await updateCompose(composeId, {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { type Application, findApplicationById } from "./application";
|
||||
import { type Compose, findComposeById } from "./compose";
|
||||
import { findServerById, type Server } from "./server";
|
||||
import { executeCommand } from "@/server/utils/servers/command";
|
||||
import { execAsyncRemote } from "@/server/utils/process/execAsync";
|
||||
|
||||
export type Deployment = typeof deployments.$inferSelect;
|
||||
|
||||
@@ -208,14 +209,23 @@ const removeLastTenComposeDeployments = async (composeId: string) => {
|
||||
export const removeDeployments = async (application: Application) => {
|
||||
const { appName, applicationId } = application;
|
||||
const logsPath = path.join(LOGS_PATH, appName);
|
||||
await removeDirectoryIfExistsContent(logsPath);
|
||||
if (application.serverId) {
|
||||
await execAsyncRemote(application.serverId, `rm -rf ${logsPath}`);
|
||||
} else {
|
||||
await removeDirectoryIfExistsContent(logsPath);
|
||||
}
|
||||
await removeDeploymentsByApplicationId(applicationId);
|
||||
};
|
||||
|
||||
export const removeDeploymentsByComposeId = async (compose: Compose) => {
|
||||
const { appName } = compose;
|
||||
const logsPath = path.join(LOGS_PATH, appName);
|
||||
await removeDirectoryIfExistsContent(logsPath);
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(compose.serverId, `rm -rf ${logsPath}`);
|
||||
} else {
|
||||
await removeDirectoryIfExistsContent(logsPath);
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(deployments)
|
||||
.where(eq(deployments.composeId, compose.composeId))
|
||||
|
||||
@@ -29,52 +29,52 @@ type DeployJob =
|
||||
|
||||
export type DeploymentJob = DeployJob;
|
||||
|
||||
// export const deploymentWorker = new Worker(
|
||||
// "deployments",
|
||||
// async (job: Job<DeploymentJob>) => {
|
||||
// try {
|
||||
// if (job.data.applicationType === "application") {
|
||||
// await updateApplicationStatus(job.data.applicationId, "running");
|
||||
// if (job.data.type === "redeploy") {
|
||||
// await rebuildApplication({
|
||||
// applicationId: job.data.applicationId,
|
||||
// titleLog: job.data.titleLog,
|
||||
// descriptionLog: job.data.descriptionLog,
|
||||
// });
|
||||
// } else if (job.data.type === "deploy") {
|
||||
// await deployApplication({
|
||||
// applicationId: job.data.applicationId,
|
||||
// titleLog: job.data.titleLog,
|
||||
// descriptionLog: job.data.descriptionLog,
|
||||
// });
|
||||
// }
|
||||
// } else if (job.data.applicationType === "compose") {
|
||||
// await updateCompose(job.data.composeId, {
|
||||
// composeStatus: "running",
|
||||
// });
|
||||
// if (job.data.type === "deploy") {
|
||||
// await deployCompose({
|
||||
// composeId: job.data.composeId,
|
||||
// titleLog: job.data.titleLog,
|
||||
// descriptionLog: job.data.descriptionLog,
|
||||
// });
|
||||
// } else if (job.data.type === "redeploy") {
|
||||
// await rebuildCompose({
|
||||
// composeId: job.data.composeId,
|
||||
// titleLog: job.data.titleLog,
|
||||
// descriptionLog: job.data.descriptionLog,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.log("Error", error);
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// autorun: false,
|
||||
// connection: redisConfig,
|
||||
// },
|
||||
// );
|
||||
export const deploymentWorker = new Worker(
|
||||
"deployments",
|
||||
async (job: Job<DeploymentJob>) => {
|
||||
try {
|
||||
if (job.data.applicationType === "application") {
|
||||
await updateApplicationStatus(job.data.applicationId, "running");
|
||||
if (job.data.type === "redeploy") {
|
||||
await rebuildApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
});
|
||||
} else if (job.data.type === "deploy") {
|
||||
await deployApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
});
|
||||
}
|
||||
} else if (job.data.applicationType === "compose") {
|
||||
await updateCompose(job.data.composeId, {
|
||||
composeStatus: "running",
|
||||
});
|
||||
if (job.data.type === "deploy") {
|
||||
await deployCompose({
|
||||
composeId: job.data.composeId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
});
|
||||
} else if (job.data.type === "redeploy") {
|
||||
await rebuildCompose({
|
||||
composeId: job.data.composeId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
}
|
||||
},
|
||||
{
|
||||
autorun: false,
|
||||
connection: redisConfig,
|
||||
},
|
||||
);
|
||||
|
||||
export const cleanQueuesByApplication = async (applicationId: string) => {
|
||||
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
|
||||
|
||||
@@ -13,10 +13,10 @@ import {
|
||||
} from "../api/services/compose";
|
||||
|
||||
export const redisConfig: ConnectionOptions = {
|
||||
host: "31.220.108.27",
|
||||
password: "xYBugfHkULig1iLN",
|
||||
// host: process.env.NODE_ENV === "production" ? "dokploy-redis" : "127.0.0.1",
|
||||
port: 1233,
|
||||
// host: "31.220.108.27",
|
||||
// password: "xYBugfHkULig1iLN",
|
||||
host: process.env.NODE_ENV === "production" ? "dokploy-redis" : "127.0.0.1",
|
||||
// port: 1233,
|
||||
};
|
||||
// TODO: maybe add a options to clean the queue to the times
|
||||
const myQueue = new Queue("deployments", {
|
||||
@@ -48,83 +48,3 @@ function createRedisConnection(server: Server) {
|
||||
port: "6379",
|
||||
} as ConnectionOptions;
|
||||
}
|
||||
|
||||
async function setupServerQueueAndWorker(server: Server) {
|
||||
const connection = createRedisConnection(server);
|
||||
|
||||
if (!workersMap.has(server.serverId)) {
|
||||
const queue = new Queue(`deployments-${server.serverId}`, {
|
||||
connection,
|
||||
});
|
||||
const worker = new Worker(
|
||||
`deployments-${server.serverId}`,
|
||||
async (job: Job<DeploymentJob>) => {
|
||||
// Ejecuta el trabajo de despliegue
|
||||
try {
|
||||
if (job.data.applicationType === "application") {
|
||||
await updateApplicationStatus(job.data.applicationId, "running");
|
||||
if (job.data.type === "redeploy") {
|
||||
await rebuildApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
});
|
||||
} else if (job.data.type === "deploy") {
|
||||
await deployApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
});
|
||||
}
|
||||
} else if (job.data.applicationType === "compose") {
|
||||
await updateCompose(job.data.composeId, {
|
||||
composeStatus: "running",
|
||||
});
|
||||
if (job.data.type === "deploy") {
|
||||
await deployCompose({
|
||||
composeId: job.data.composeId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
});
|
||||
} else if (job.data.type === "redeploy") {
|
||||
await rebuildCompose({
|
||||
composeId: job.data.composeId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
}
|
||||
},
|
||||
{
|
||||
limiter: {
|
||||
max: 1,
|
||||
duration: 1000,
|
||||
},
|
||||
connection,
|
||||
},
|
||||
);
|
||||
// Almacena worker y queue para reutilizar
|
||||
workersMap.set(server.serverId, worker);
|
||||
queuesMap.set(server.serverId, queue);
|
||||
}
|
||||
return {
|
||||
queue: queuesMap.get(server.serverId),
|
||||
worker: workersMap.get(server.serverId),
|
||||
};
|
||||
}
|
||||
|
||||
export async function enqueueDeploymentJob(
|
||||
serverId: string,
|
||||
jobData: DeploymentJob,
|
||||
) {
|
||||
const server = await findServerById(serverId);
|
||||
const { queue } = await setupServerQueueAndWorker(server);
|
||||
|
||||
await queue?.add(`deployments-${serverId}`, jobData, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
setupTerminalWebSocketServer,
|
||||
} from "./wss/terminal";
|
||||
import { IS_CLOUD } from "./constants";
|
||||
import { deploymentWorker } from "./queues/deployments-queue";
|
||||
|
||||
config({ path: ".env" });
|
||||
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
|
||||
@@ -68,7 +69,7 @@ void app.prepare().then(async () => {
|
||||
|
||||
server.listen(PORT);
|
||||
console.log("Server Started:", PORT);
|
||||
// deploymentWorker.run();
|
||||
deploymentWorker.run();
|
||||
} catch (e) {
|
||||
console.error("Main Server Error", e);
|
||||
}
|
||||
|
||||
@@ -98,14 +98,24 @@ Compose Type: ${composeType} ✅`;
|
||||
});
|
||||
|
||||
const bashCommand = `
|
||||
set -e;
|
||||
echo "${logBox}" >> ${logPath};
|
||||
${newCompose}
|
||||
${envCommand}
|
||||
cd ${projectPath} || exit 1;
|
||||
docker ${command.split(" ").join(" ")} >> ${logPath} 2>&1;
|
||||
echo "Docker Compose Deployed: ✅" >> ${logPath};
|
||||
`;
|
||||
set -e
|
||||
{
|
||||
echo "${logBox}" >> "${logPath}"
|
||||
|
||||
${newCompose}
|
||||
|
||||
${envCommand}
|
||||
|
||||
cd "${projectPath}";
|
||||
|
||||
docker ${command.split(" ").join(" ")} >> "${logPath}" 2>&1 || { echo "Error: ❌ Docker command failed" >> "${logPath}"; exit 1; }
|
||||
|
||||
echo "Docker Compose Deployed: ✅" >> "${logPath}"
|
||||
} || {
|
||||
echo "Error: ❌ Script execution failed" >> "${logPath}"
|
||||
exit 1
|
||||
}
|
||||
`;
|
||||
|
||||
return await execAsyncRemote(compose.serverId, bashCommand);
|
||||
};
|
||||
|
||||
@@ -30,8 +30,6 @@ export const buildHeroku = async (
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
// Stream the data
|
||||
console.log(data);
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
||||
@@ -94,7 +94,7 @@ export const getNixpacksCommand = (
|
||||
/* No need for any start command, since we'll use nginx later on */
|
||||
args.push("--no-error-without-start");
|
||||
}
|
||||
|
||||
console.log("args", args);
|
||||
const command = `nixpacks ${args.join(" ")}`;
|
||||
const bashCommand = `
|
||||
echo "Starting nixpacks build..." >> ${logPath};
|
||||
@@ -108,6 +108,9 @@ echo "Nixpacks build completed." >> ${logPath};
|
||||
Then, remove the container and create a static build.
|
||||
*/
|
||||
|
||||
if (publishDirectory) {
|
||||
}
|
||||
|
||||
// if (publishDirectory) {
|
||||
// bashCommand += `
|
||||
// docker create --name ${buildContainerId} ${appName}
|
||||
|
||||
@@ -137,7 +137,7 @@ export const writeDomainsToComposeRemote = async (
|
||||
domains: Domain[],
|
||||
) => {
|
||||
if (!domains.length) {
|
||||
return;
|
||||
return "";
|
||||
}
|
||||
const composeConverted = await addDomainToCompose(compose, domains);
|
||||
const path = getComposePath(compose);
|
||||
|
||||
@@ -214,9 +214,17 @@ export const startServiceRemote = async (serverId: string, appName: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const removeService = async (appName: string) => {
|
||||
export const removeService = async (
|
||||
appName: string,
|
||||
serverId?: string | null,
|
||||
) => {
|
||||
try {
|
||||
await execAsync(`docker service rm ${appName}`);
|
||||
const command = `docker service rm ${appName}`;
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
COMPOSE_PATH,
|
||||
MONITORING_PATH,
|
||||
} from "@/server/constants";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
|
||||
export const recreateDirectory = async (pathFolder: string): Promise<void> => {
|
||||
try {
|
||||
@@ -34,31 +34,54 @@ export const removeFileOrDirectory = async (path: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const removeDirectoryCode = async (appName: string) => {
|
||||
export const removeDirectoryCode = async (
|
||||
appName: string,
|
||||
serverId?: string | null,
|
||||
) => {
|
||||
const directoryPath = path.join(APPLICATIONS_PATH, appName);
|
||||
|
||||
const command = `rm -rf ${directoryPath}`;
|
||||
try {
|
||||
await execAsync(`rm -rf ${directoryPath}`);
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error to remove ${directoryPath}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const removeComposeDirectory = async (appName: string) => {
|
||||
export const removeComposeDirectory = async (
|
||||
appName: string,
|
||||
serverId?: string | null,
|
||||
) => {
|
||||
const directoryPath = path.join(COMPOSE_PATH, appName);
|
||||
const command = `rm -rf ${directoryPath}`;
|
||||
try {
|
||||
await execAsync(`rm -rf ${directoryPath}`);
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error to remove ${directoryPath}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const removeMonitoringDirectory = async (appName: string) => {
|
||||
export const removeMonitoringDirectory = async (
|
||||
appName: string,
|
||||
serverId?: string | null,
|
||||
) => {
|
||||
const directoryPath = path.join(MONITORING_PATH, appName);
|
||||
const command = `rm -rf ${directoryPath}`;
|
||||
try {
|
||||
await execAsync(`rm -rf ${directoryPath}`);
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error to remove ${directoryPath}: ${error}`);
|
||||
throw error;
|
||||
|
||||
@@ -33,7 +33,11 @@ export const execAsyncRemote = async (
|
||||
if (code === 0) {
|
||||
resolve({ stdout, stderr });
|
||||
} else {
|
||||
reject(new Error(`Command exited with code ${code}`));
|
||||
reject(
|
||||
new Error(
|
||||
`Command exited with code ${code}. Stderr: ${stderr}, command: ${command}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
})
|
||||
.on("data", (data: string) => {
|
||||
|
||||
@@ -181,6 +181,11 @@ export const getBitbucketCloneCommand = async (
|
||||
}
|
||||
|
||||
if (!bitbucketId) {
|
||||
const command = `
|
||||
echo "Error: ❌ Bitbucket Provider not found" >> ${logPath};
|
||||
exit 1;
|
||||
`;
|
||||
await execAsyncRemote(serverId, command);
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Bitbucket Provider not found",
|
||||
@@ -198,7 +203,7 @@ export const getBitbucketCloneCommand = async (
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
if ! git clone --branch ${bitbucketBranch} --depth 1 --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "[ERROR] Fail to clone the repository ${repoclone}" >> ${logPath};
|
||||
echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};
|
||||
|
||||
@@ -6,7 +6,7 @@ import { TRPCError } from "@trpc/server";
|
||||
import { recreateDirectory } from "../filesystem/directory";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
import { Compose } from "@/server/api/services/compose";
|
||||
import type { Compose } from "@/server/api/services/compose";
|
||||
|
||||
export const cloneGitRepository = async (
|
||||
entity: {
|
||||
@@ -108,6 +108,12 @@ export const getCustomGitCloneCommand = async (
|
||||
} = entity;
|
||||
|
||||
if (!customGitUrl || !customGitBranch) {
|
||||
const command = `
|
||||
echo "Error: ❌ Repository not found" >> ${logPath};
|
||||
exit 1;
|
||||
`;
|
||||
|
||||
await execAsyncRemote(serverId, command);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error: Repository not found",
|
||||
@@ -143,7 +149,7 @@ export const getCustomGitCloneCommand = async (
|
||||
|
||||
command.push(
|
||||
`if ! git clone --branch ${customGitBranch} --depth 1 --progress ${customGitUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "[ERROR] Fail to clone the repository ${customGitUrl}" >> ${logPath};
|
||||
echo "❌ [ERROR] Fail to clone the repository ${customGitUrl}" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
`,
|
||||
|
||||
@@ -151,7 +151,20 @@ export const getGithubCloneCommand = async (
|
||||
) => {
|
||||
const { appName, repository, owner, branch, githubId, serverId } = entity;
|
||||
|
||||
if (!githubId || !serverId) {
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (!githubId) {
|
||||
const command = `
|
||||
echo "Error: ❌ Github Provider not found" >> ${logPath};
|
||||
exit 1;
|
||||
`;
|
||||
|
||||
await executeCommand(serverId, command);
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "GitHub Provider not found",
|
||||
@@ -192,7 +205,7 @@ export const getGithubCloneCommand = async (
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
if ! git clone --branch ${branch} --depth 1 --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "[ERROR] Fallo al clonar el repositorio ${repoclone}" >> ${logPath};
|
||||
echo "❌ [ERROR] Fallo al clonar el repositorio ${repoclone}" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};
|
||||
|
||||
@@ -179,6 +179,12 @@ export const getGitlabCloneCommand = async (
|
||||
}
|
||||
|
||||
if (!gitlabId) {
|
||||
const command = `
|
||||
echo "Error: ❌ Gitlab Provider not found" >> ${logPath};
|
||||
exit 1;
|
||||
`;
|
||||
|
||||
await execAsyncRemote(serverId, command);
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitlab Provider not found",
|
||||
@@ -203,7 +209,7 @@ export const getGitlabCloneCommand = async (
|
||||
exit 1; # Exit with error code
|
||||
`;
|
||||
|
||||
await executeCommand(serverId, bashCommand);
|
||||
await execAsyncRemote(serverId, bashCommand);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -218,7 +224,7 @@ export const getGitlabCloneCommand = async (
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
if ! git clone --branch ${gitlabBranch} --depth 1 --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "[ERROR] Fail to clone the repository ${repoclone}" >> ${logPath};
|
||||
echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};
|
||||
|
||||
@@ -47,9 +47,20 @@ export const createTraefikConfig = (appName: string) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const removeTraefikConfig = async (appName: string) => {
|
||||
export const removeTraefikConfig = async (
|
||||
appName: string,
|
||||
serverId?: string | null,
|
||||
) => {
|
||||
try {
|
||||
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, `rm ${configPath}`);
|
||||
} else {
|
||||
if (fs.existsSync(configPath)) {
|
||||
await fs.promises.unlink(configPath);
|
||||
}
|
||||
}
|
||||
if (fs.existsSync(configPath)) {
|
||||
await fs.promises.unlink(configPath);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { dump, load } from "js-yaml";
|
||||
import type { ApplicationNested } from "../builders";
|
||||
import type { FileConfig } from "./file-types";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
import { writeTraefikConfigRemote } from "./application";
|
||||
|
||||
export const addMiddleware = (config: FileConfig, middlewareName: string) => {
|
||||
if (config.http?.routers) {
|
||||
@@ -43,7 +44,7 @@ export const deleteMiddleware = (
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteAllMiddlewares = (application: ApplicationNested) => {
|
||||
export const deleteAllMiddlewares = async (application: ApplicationNested) => {
|
||||
const config = loadMiddlewares<FileConfig>();
|
||||
const { security, appName, redirects } = application;
|
||||
|
||||
@@ -60,7 +61,11 @@ export const deleteAllMiddlewares = (application: ApplicationNested) => {
|
||||
}
|
||||
}
|
||||
|
||||
writeMiddleware(config);
|
||||
if (application.serverId) {
|
||||
await writeTraefikConfigRemote(config, "middlewares", application.serverId);
|
||||
} else {
|
||||
writeMiddleware(config);
|
||||
}
|
||||
};
|
||||
|
||||
export const loadMiddlewares = <T>() => {
|
||||
|
||||
Reference in New Issue
Block a user