refactor(server): add support for multi server

This commit is contained in:
Mauricio Siu
2024-09-15 01:24:55 -06:00
parent 033bf66405
commit d2c8632c4f
26 changed files with 340 additions and 244 deletions

View File

@@ -16,6 +16,7 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Loader2 } from "lucide-react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export const DockerLogs = dynamic( export const DockerLogs = dynamic(
@@ -34,7 +35,7 @@ interface Props {
} }
export const ShowDockerLogs = ({ appName, serverId }: Props) => { export const ShowDockerLogs = ({ appName, serverId }: Props) => {
const { data } = api.docker.getContainersByAppNameMatch.useQuery( const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
{ {
appName, appName,
serverId, serverId,
@@ -64,7 +65,14 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
<Label>Select a container to view logs</Label> <Label>Select a container to view logs</Label>
<Select onValueChange={setContainerId} value={containerId}> <Select onValueChange={setContainerId} value={containerId}>
<SelectTrigger> <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> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>

View File

@@ -16,6 +16,7 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Loader, Loader2 } from "lucide-react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export const DockerLogs = dynamic( export const DockerLogs = dynamic(
@@ -39,7 +40,7 @@ export const ShowDockerLogsCompose = ({
appType, appType,
serverId, serverId,
}: Props) => { }: Props) => {
const { data } = api.docker.getContainersByAppNameMatch.useQuery( const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
{ {
appName, appName,
appType, appType,
@@ -70,7 +71,14 @@ export const ShowDockerLogsCompose = ({
<Label>Select a container to view logs</Label> <Label>Select a container to view logs</Label>
<Select onValueChange={setContainerId} value={containerId}> <Select onValueChange={setContainerId} value={containerId}>
<SelectTrigger> <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> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>

View File

@@ -20,6 +20,7 @@ import { api } from "@/utils/api";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { DockerMonitoring } from "../../monitoring/docker/show"; import { DockerMonitoring } from "../../monitoring/docker/show";
import { Loader2 } from "lucide-react";
interface Props { interface Props {
appName: string; appName: string;
@@ -32,7 +33,7 @@ export const ShowMonitoringCompose = ({
appType = "stack", appType = "stack",
serverId, serverId,
}: Props) => { }: Props) => {
const { data } = api.docker.getContainersByAppNameMatch.useQuery( const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
{ {
appName: appName, appName: appName,
appType, appType,
@@ -49,7 +50,7 @@ export const ShowMonitoringCompose = ({
const [containerId, setContainerId] = useState<string | undefined>(); const [containerId, setContainerId] = useState<string | undefined>();
const { mutateAsync: restart, isLoading } = const { mutateAsync: restart, isLoading: isRestarting } =
api.docker.restartContainer.useMutation(); api.docker.restartContainer.useMutation();
useEffect(() => { useEffect(() => {
@@ -80,7 +81,14 @@ export const ShowMonitoringCompose = ({
value={containerAppName} value={containerAppName}
> >
<SelectTrigger> <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> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
@@ -98,7 +106,7 @@ export const ShowMonitoringCompose = ({
</SelectContent> </SelectContent>
</Select> </Select>
<Button <Button
isLoading={isLoading} isLoading={isRestarting}
onClick={async () => { onClick={async () => {
if (!containerId) return; if (!containerId) return;
toast.success(`Restarting container ${containerAppName}`); toast.success(`Restarting container ${containerAppName}`);

View File

@@ -17,6 +17,7 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Loader2 } from "lucide-react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import type React from "react"; import type React from "react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@@ -38,7 +39,7 @@ interface Props {
} }
export const DockerTerminalModal = ({ children, appName, serverId }: Props) => { export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
const { data } = api.docker.getContainersByAppNameMatch.useQuery( const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
{ {
appName, appName,
serverId, serverId,
@@ -67,7 +68,14 @@ export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
<Label>Select a container to view logs</Label> <Label>Select a container to view logs</Label>
<Select onValueChange={setContainerId} value={containerId}> <Select onValueChange={setContainerId} value={containerId}>
<SelectTrigger> <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> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>

View File

@@ -18,6 +18,7 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Loader2 } from "lucide-react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import type React from "react"; import type React from "react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@@ -38,7 +39,7 @@ interface Props {
} }
export const ShowModalLogs = ({ appName, children }: Props) => { export const ShowModalLogs = ({ appName, children }: Props) => {
const { data } = api.docker.getContainersByAppLabel.useQuery( const { data, isLoading } = api.docker.getContainersByAppLabel.useQuery(
{ {
appName, appName,
}, },
@@ -72,7 +73,14 @@ export const ShowModalLogs = ({ appName, children }: Props) => {
<Label>Select a container to view logs</Label> <Label>Select a container to view logs</Label>
<Select onValueChange={setContainerId} value={containerId}> <Select onValueChange={setContainerId} value={containerId}>
<SelectTrigger> <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> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>

View File

@@ -23,7 +23,7 @@ import {
type DeploymentJob, type DeploymentJob,
cleanQueuesByApplication, cleanQueuesByApplication,
} from "@/server/queues/deployments-queue"; } from "@/server/queues/deployments-queue";
import { enqueueDeploymentJob, myQueue } from "@/server/queues/queueSetup"; import { myQueue } from "@/server/queues/queueSetup";
import { import {
removeService, removeService,
startService, startService,
@@ -134,12 +134,19 @@ export const applicationRouter = createTRPCRouter({
.returning(); .returning();
const cleanupOperations = [ const cleanupOperations = [
async () => deleteAllMiddlewares(application), async () => await deleteAllMiddlewares(application),
async () => await removeDeployments(application), async () => await removeDeployments(application),
async () => await removeDirectoryCode(application?.appName), async () =>
async () => await removeMonitoringDirectory(application?.appName), await removeDirectoryCode(application.appName, application.serverId),
async () => await removeTraefikConfig(application?.appName), async () =>
async () => await removeService(application?.appName), 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) { for (const operation of cleanupOperations) {
@@ -327,7 +334,7 @@ export const applicationRouter = createTRPCRouter({
deploy: protectedProcedure deploy: protectedProcedure
.input(apiFindOneApplication) .input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId); // const application = await findApplicationById(input.applicationId);
const jobData: DeploymentJob = { const jobData: DeploymentJob = {
applicationId: input.applicationId, applicationId: input.applicationId,
titleLog: "Manual deployment", titleLog: "Manual deployment",
@@ -335,10 +342,18 @@ export const applicationRouter = createTRPCRouter({
type: "deploy", type: "deploy",
applicationType: "application", applicationType: "application",
}; };
if (!application.serverId) { await myQueue.add(
} else { "deployments",
await enqueueDeploymentJob(application.serverId, jobData); { ...jobData },
} {
removeOnComplete: true,
removeOnFail: true,
},
);
// if (!application.serverId) {
// } else {
// await enqueueDeploymentJob(application.serverId, jobData);
// }
}), }),
cleanQueues: protectedProcedure cleanQueues: protectedProcedure

View File

@@ -13,7 +13,7 @@ import {
type DeploymentJob, type DeploymentJob,
cleanQueuesByCompose, cleanQueuesByCompose,
} from "@/server/queues/deployments-queue"; } 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 { createCommand } from "@/server/utils/builders/compose";
import { import {
randomizeComposeFile, randomizeComposeFile,
@@ -176,14 +176,7 @@ export const composeRouter = createTRPCRouter({
// applicationType: "compose", // applicationType: "compose",
// descriptionLog: "", // descriptionLog: "",
// }; // };
// await myQueue.add(
// "deployments",
// { ...jobData },
// {
// removeOnComplete: true,
// removeOnFail: true,
// },
// );
const compose = await findComposeById(input.composeId); const compose = await findComposeById(input.composeId);
const jobData: DeploymentJob = { const jobData: DeploymentJob = {
composeId: input.composeId, composeId: input.composeId,
@@ -192,10 +185,18 @@ export const composeRouter = createTRPCRouter({
applicationType: "compose", applicationType: "compose",
descriptionLog: "", descriptionLog: "",
}; };
if (!compose.serverId) { await myQueue.add(
} else { "deployments",
await enqueueDeploymentJob(compose.serverId, jobData); { ...jobData },
} {
removeOnComplete: true,
removeOnFail: true,
},
);
// if (!compose.serverId) {
// } else {
// await enqueueDeploymentJob(compose.serverId, jobData);
// }
}), }),
redeploy: protectedProcedure redeploy: protectedProcedure
.input(apiFindCompose) .input(apiFindCompose)

View File

@@ -38,6 +38,7 @@ import {
} from "@/server/utils/providers/gitlab"; } from "@/server/utils/providers/gitlab";
import { validUniqueServerAppName } from "./project"; import { validUniqueServerAppName } from "./project";
import { executeCommand } from "@/server/utils/servers/command"; import { executeCommand } from "@/server/utils/servers/command";
import { execAsyncRemote } from "@/server/utils/process/execAsync";
export type Application = typeof applications.$inferSelect; export type Application = typeof applications.$inferSelect;
export const createApplication = async ( export const createApplication = async (
@@ -164,7 +165,7 @@ export const deployApplication = async ({
try { try {
if (application.serverId) { if (application.serverId) {
let command = "set -e"; let command = "set -e;";
if (application.sourceType === "github") { if (application.sourceType === "github") {
command += await getGithubCloneCommand(application, deployment.logPath); command += await getGithubCloneCommand(application, deployment.logPath);
} else if (application.sourceType === "gitlab") { } else if (application.sourceType === "gitlab") {
@@ -186,8 +187,7 @@ export const deployApplication = async ({
if (application.sourceType !== "docker") { if (application.sourceType !== "docker") {
command += getBuildCommand(application, deployment.logPath); command += getBuildCommand(application, deployment.logPath);
} }
await execAsyncRemote(application.serverId, command);
await executeCommand(application.serverId, command);
await mechanizeDockerContainer(application); await mechanizeDockerContainer(application);
} else { } else {
if (application.sourceType === "github") { if (application.sourceType === "github") {
@@ -209,8 +209,6 @@ export const deployApplication = async ({
} }
} }
console.log("Command", "Finish");
await updateDeploymentStatus(deployment.deploymentId, "done"); await updateDeploymentStatus(deployment.deploymentId, "done");
await updateApplicationStatus(applicationId, "done"); await updateApplicationStatus(applicationId, "done");
@@ -263,18 +261,31 @@ export const rebuildApplication = async ({
}); });
try { try {
if (application.sourceType === "github") { if (application.serverId) {
await buildApplication(application, deployment.logPath); let command = "set -e;";
} else if (application.sourceType === "gitlab") { if (application.sourceType === "docker") {
await buildApplication(application, deployment.logPath); command += await buildRemoteDocker(application, deployment.logPath);
} else if (application.sourceType === "bitbucket") { }
await buildApplication(application, deployment.logPath);
} else if (application.sourceType === "docker") { if (application.sourceType !== "docker") {
await buildDocker(application, deployment.logPath); command += getBuildCommand(application, deployment.logPath);
} else if (application.sourceType === "git") { }
await buildApplication(application, deployment.logPath); await execAsyncRemote(application.serverId, command);
} else if (application.sourceType === "drop") { await mechanizeDockerContainer(application);
await buildApplication(application, deployment.logPath); } 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 updateDeploymentStatus(deployment.deploymentId, "done");
await updateApplicationStatus(applicationId, "done"); await updateApplicationStatus(applicationId, "done");

View File

@@ -241,20 +241,6 @@ export const deployCompose = async ({
} else if (compose.sourceType === "raw") { } else if (compose.sourceType === "raw") {
command += getCreateComposeFileCommand(compose); 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() { async function* sequentialSteps() {
yield execAsyncRemote(compose.serverId, command); yield execAsyncRemote(compose.serverId, command);
yield getBuildComposeCommand(compose, deployment.logPath); yield getBuildComposeCommand(compose, deployment.logPath);
@@ -262,9 +248,6 @@ export const deployCompose = async ({
const steps = sequentialSteps(); const steps = sequentialSteps();
for await (const step of steps) { for await (const step of steps) {
if (step.stderr) {
console.log(step.stderr);
}
step; step;
} }
@@ -330,7 +313,12 @@ export const rebuildCompose = async ({
}); });
try { 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 updateDeploymentStatus(deployment.deploymentId, "done");
await updateCompose(composeId, { await updateCompose(composeId, {
composeStatus: "done", composeStatus: "done",
@@ -351,13 +339,24 @@ export const removeCompose = async (compose: Compose) => {
const projectPath = join(COMPOSE_PATH, compose.appName); const projectPath = join(COMPOSE_PATH, compose.appName);
if (compose.composeType === "stack") { 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, cwd: projectPath,
}); });
} else { } else {
await execAsync(`docker compose -p ${compose.appName} down`, { const command = `cd ${projectPath} && docker compose -p ${compose.appName} down && rm -rf ${projectPath}`;
cwd: projectPath, if (compose.serverId) {
}); await execAsyncRemote(compose.serverId, command);
} else {
await execAsync(command, {
cwd: projectPath,
});
}
} }
} catch (error) { } catch (error) {
throw error; throw error;
@@ -370,9 +369,16 @@ export const stopCompose = async (composeId: string) => {
const compose = await findComposeById(composeId); const compose = await findComposeById(composeId);
try { try {
if (compose.composeType === "docker-compose") { if (compose.composeType === "docker-compose") {
await execAsync(`docker compose -p ${compose.appName} stop`, { if (compose.serverId) {
cwd: join(COMPOSE_PATH, compose.appName), 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, { await updateCompose(composeId, {

View File

@@ -16,6 +16,7 @@ import { type Application, findApplicationById } from "./application";
import { type Compose, findComposeById } from "./compose"; import { type Compose, findComposeById } from "./compose";
import { findServerById, type Server } from "./server"; import { findServerById, type Server } from "./server";
import { executeCommand } from "@/server/utils/servers/command"; import { executeCommand } from "@/server/utils/servers/command";
import { execAsyncRemote } from "@/server/utils/process/execAsync";
export type Deployment = typeof deployments.$inferSelect; export type Deployment = typeof deployments.$inferSelect;
@@ -208,14 +209,23 @@ const removeLastTenComposeDeployments = async (composeId: string) => {
export const removeDeployments = async (application: Application) => { export const removeDeployments = async (application: Application) => {
const { appName, applicationId } = application; const { appName, applicationId } = application;
const logsPath = path.join(LOGS_PATH, appName); 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); await removeDeploymentsByApplicationId(applicationId);
}; };
export const removeDeploymentsByComposeId = async (compose: Compose) => { export const removeDeploymentsByComposeId = async (compose: Compose) => {
const { appName } = compose; const { appName } = compose;
const logsPath = path.join(LOGS_PATH, appName); 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 await db
.delete(deployments) .delete(deployments)
.where(eq(deployments.composeId, compose.composeId)) .where(eq(deployments.composeId, compose.composeId))

View File

@@ -29,52 +29,52 @@ type DeployJob =
export type DeploymentJob = DeployJob; export type DeploymentJob = DeployJob;
// export const deploymentWorker = new Worker( export const deploymentWorker = new Worker(
// "deployments", "deployments",
// async (job: Job<DeploymentJob>) => { async (job: Job<DeploymentJob>) => {
// try { try {
// if (job.data.applicationType === "application") { if (job.data.applicationType === "application") {
// await updateApplicationStatus(job.data.applicationId, "running"); await updateApplicationStatus(job.data.applicationId, "running");
// if (job.data.type === "redeploy") { if (job.data.type === "redeploy") {
// await rebuildApplication({ await rebuildApplication({
// applicationId: job.data.applicationId, applicationId: job.data.applicationId,
// titleLog: job.data.titleLog, titleLog: job.data.titleLog,
// descriptionLog: job.data.descriptionLog, descriptionLog: job.data.descriptionLog,
// }); });
// } else if (job.data.type === "deploy") { } else if (job.data.type === "deploy") {
// await deployApplication({ await deployApplication({
// applicationId: job.data.applicationId, applicationId: job.data.applicationId,
// titleLog: job.data.titleLog, titleLog: job.data.titleLog,
// descriptionLog: job.data.descriptionLog, descriptionLog: job.data.descriptionLog,
// }); });
// } }
// } else if (job.data.applicationType === "compose") { } else if (job.data.applicationType === "compose") {
// await updateCompose(job.data.composeId, { await updateCompose(job.data.composeId, {
// composeStatus: "running", composeStatus: "running",
// }); });
// if (job.data.type === "deploy") { if (job.data.type === "deploy") {
// await deployCompose({ await deployCompose({
// composeId: job.data.composeId, composeId: job.data.composeId,
// titleLog: job.data.titleLog, titleLog: job.data.titleLog,
// descriptionLog: job.data.descriptionLog, descriptionLog: job.data.descriptionLog,
// }); });
// } else if (job.data.type === "redeploy") { } else if (job.data.type === "redeploy") {
// await rebuildCompose({ await rebuildCompose({
// composeId: job.data.composeId, composeId: job.data.composeId,
// titleLog: job.data.titleLog, titleLog: job.data.titleLog,
// descriptionLog: job.data.descriptionLog, descriptionLog: job.data.descriptionLog,
// }); });
// } }
// } }
// } catch (error) { } catch (error) {
// console.log("Error", error); console.log("Error", error);
// } }
// }, },
// { {
// autorun: false, autorun: false,
// connection: redisConfig, connection: redisConfig,
// }, },
// ); );
export const cleanQueuesByApplication = async (applicationId: string) => { export const cleanQueuesByApplication = async (applicationId: string) => {
const jobs = await myQueue.getJobs(["waiting", "delayed"]); const jobs = await myQueue.getJobs(["waiting", "delayed"]);

View File

@@ -13,10 +13,10 @@ import {
} from "../api/services/compose"; } from "../api/services/compose";
export const redisConfig: ConnectionOptions = { export const redisConfig: ConnectionOptions = {
host: "31.220.108.27", // host: "31.220.108.27",
password: "xYBugfHkULig1iLN", // password: "xYBugfHkULig1iLN",
// host: process.env.NODE_ENV === "production" ? "dokploy-redis" : "127.0.0.1", host: process.env.NODE_ENV === "production" ? "dokploy-redis" : "127.0.0.1",
port: 1233, // port: 1233,
}; };
// TODO: maybe add a options to clean the queue to the times // TODO: maybe add a options to clean the queue to the times
const myQueue = new Queue("deployments", { const myQueue = new Queue("deployments", {
@@ -48,83 +48,3 @@ function createRedisConnection(server: Server) {
port: "6379", port: "6379",
} as ConnectionOptions; } 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,
});
}

View File

@@ -24,6 +24,7 @@ import {
setupTerminalWebSocketServer, setupTerminalWebSocketServer,
} from "./wss/terminal"; } from "./wss/terminal";
import { IS_CLOUD } from "./constants"; import { IS_CLOUD } from "./constants";
import { deploymentWorker } from "./queues/deployments-queue";
config({ path: ".env" }); config({ path: ".env" });
const PORT = Number.parseInt(process.env.PORT || "3000", 10); const PORT = Number.parseInt(process.env.PORT || "3000", 10);
@@ -68,7 +69,7 @@ void app.prepare().then(async () => {
server.listen(PORT); server.listen(PORT);
console.log("Server Started:", PORT); console.log("Server Started:", PORT);
// deploymentWorker.run(); deploymentWorker.run();
} catch (e) { } catch (e) {
console.error("Main Server Error", e); console.error("Main Server Error", e);
} }

View File

@@ -98,14 +98,24 @@ Compose Type: ${composeType} ✅`;
}); });
const bashCommand = ` const bashCommand = `
set -e; set -e
echo "${logBox}" >> ${logPath}; {
${newCompose} echo "${logBox}" >> "${logPath}"
${envCommand}
cd ${projectPath} || exit 1; ${newCompose}
docker ${command.split(" ").join(" ")} >> ${logPath} 2>&1;
echo "Docker Compose Deployed: ✅" >> ${logPath}; ${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); return await execAsyncRemote(compose.serverId, bashCommand);
}; };

View File

@@ -30,8 +30,6 @@ export const buildHeroku = async (
if (writeStream.writable) { if (writeStream.writable) {
writeStream.write(data); writeStream.write(data);
} }
// Stream the data
console.log(data);
}); });
return true; return true;
} catch (e) { } catch (e) {

View File

@@ -94,7 +94,7 @@ export const getNixpacksCommand = (
/* No need for any start command, since we'll use nginx later on */ /* No need for any start command, since we'll use nginx later on */
args.push("--no-error-without-start"); args.push("--no-error-without-start");
} }
console.log("args", args);
const command = `nixpacks ${args.join(" ")}`; const command = `nixpacks ${args.join(" ")}`;
const bashCommand = ` const bashCommand = `
echo "Starting nixpacks build..." >> ${logPath}; echo "Starting nixpacks build..." >> ${logPath};
@@ -108,6 +108,9 @@ echo "Nixpacks build completed." >> ${logPath};
Then, remove the container and create a static build. Then, remove the container and create a static build.
*/ */
if (publishDirectory) {
}
// if (publishDirectory) { // if (publishDirectory) {
// bashCommand += ` // bashCommand += `
// docker create --name ${buildContainerId} ${appName} // docker create --name ${buildContainerId} ${appName}

View File

@@ -137,7 +137,7 @@ export const writeDomainsToComposeRemote = async (
domains: Domain[], domains: Domain[],
) => { ) => {
if (!domains.length) { if (!domains.length) {
return; return "";
} }
const composeConverted = await addDomainToCompose(compose, domains); const composeConverted = await addDomainToCompose(compose, domains);
const path = getComposePath(compose); const path = getComposePath(compose);

View File

@@ -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 { 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) { } catch (error) {
return error; return error;
} }

View File

@@ -6,7 +6,7 @@ import {
COMPOSE_PATH, COMPOSE_PATH,
MONITORING_PATH, MONITORING_PATH,
} from "@/server/constants"; } from "@/server/constants";
import { execAsync } from "../process/execAsync"; import { execAsync, execAsyncRemote } from "../process/execAsync";
export const recreateDirectory = async (pathFolder: string): Promise<void> => { export const recreateDirectory = async (pathFolder: string): Promise<void> => {
try { 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 directoryPath = path.join(APPLICATIONS_PATH, appName);
const command = `rm -rf ${directoryPath}`;
try { try {
await execAsync(`rm -rf ${directoryPath}`); if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
} catch (error) { } catch (error) {
console.error(`Error to remove ${directoryPath}: ${error}`); console.error(`Error to remove ${directoryPath}: ${error}`);
throw 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 directoryPath = path.join(COMPOSE_PATH, appName);
const command = `rm -rf ${directoryPath}`;
try { try {
await execAsync(`rm -rf ${directoryPath}`); if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
} catch (error) { } catch (error) {
console.error(`Error to remove ${directoryPath}: ${error}`); console.error(`Error to remove ${directoryPath}: ${error}`);
throw 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 directoryPath = path.join(MONITORING_PATH, appName);
const command = `rm -rf ${directoryPath}`;
try { try {
await execAsync(`rm -rf ${directoryPath}`); if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
} catch (error) { } catch (error) {
console.error(`Error to remove ${directoryPath}: ${error}`); console.error(`Error to remove ${directoryPath}: ${error}`);
throw error; throw error;

View File

@@ -33,7 +33,11 @@ export const execAsyncRemote = async (
if (code === 0) { if (code === 0) {
resolve({ stdout, stderr }); resolve({ stdout, stderr });
} else { } 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) => { .on("data", (data: string) => {

View File

@@ -181,6 +181,11 @@ export const getBitbucketCloneCommand = async (
} }
if (!bitbucketId) { if (!bitbucketId) {
const command = `
echo "Error: ❌ Bitbucket Provider not found" >> ${logPath};
exit 1;
`;
await execAsyncRemote(serverId, command);
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
message: "Bitbucket Provider not found", message: "Bitbucket Provider not found",
@@ -198,7 +203,7 @@ export const getBitbucketCloneCommand = async (
rm -rf ${outputPath}; rm -rf ${outputPath};
mkdir -p ${outputPath}; mkdir -p ${outputPath};
if ! git clone --branch ${bitbucketBranch} --depth 1 --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then 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; exit 1;
fi fi
echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath}; echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};

View File

@@ -6,7 +6,7 @@ import { TRPCError } from "@trpc/server";
import { recreateDirectory } from "../filesystem/directory"; import { recreateDirectory } from "../filesystem/directory";
import { execAsync, execAsyncRemote } from "../process/execAsync"; import { execAsync, execAsyncRemote } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync"; import { spawnAsync } from "../process/spawnAsync";
import { Compose } from "@/server/api/services/compose"; import type { Compose } from "@/server/api/services/compose";
export const cloneGitRepository = async ( export const cloneGitRepository = async (
entity: { entity: {
@@ -108,6 +108,12 @@ export const getCustomGitCloneCommand = async (
} = entity; } = entity;
if (!customGitUrl || !customGitBranch) { if (!customGitUrl || !customGitBranch) {
const command = `
echo "Error: ❌ Repository not found" >> ${logPath};
exit 1;
`;
await execAsyncRemote(serverId, command);
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Error: Repository not found", message: "Error: Repository not found",
@@ -143,7 +149,7 @@ export const getCustomGitCloneCommand = async (
command.push( command.push(
`if ! git clone --branch ${customGitBranch} --depth 1 --progress ${customGitUrl} ${outputPath} >> ${logPath} 2>&1; then `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; exit 1;
fi fi
`, `,

View File

@@ -151,7 +151,20 @@ export const getGithubCloneCommand = async (
) => { ) => {
const { appName, repository, owner, branch, githubId, serverId } = entity; 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({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
message: "GitHub Provider not found", message: "GitHub Provider not found",
@@ -192,7 +205,7 @@ export const getGithubCloneCommand = async (
rm -rf ${outputPath}; rm -rf ${outputPath};
mkdir -p ${outputPath}; mkdir -p ${outputPath};
if ! git clone --branch ${branch} --depth 1 --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then 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; exit 1;
fi fi
echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath}; echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};

View File

@@ -179,6 +179,12 @@ export const getGitlabCloneCommand = async (
} }
if (!gitlabId) { if (!gitlabId) {
const command = `
echo "Error: ❌ Gitlab Provider not found" >> ${logPath};
exit 1;
`;
await execAsyncRemote(serverId, command);
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
message: "Gitlab Provider not found", message: "Gitlab Provider not found",
@@ -203,7 +209,7 @@ export const getGitlabCloneCommand = async (
exit 1; # Exit with error code exit 1; # Exit with error code
`; `;
await executeCommand(serverId, bashCommand); await execAsyncRemote(serverId, bashCommand);
return; return;
} }
@@ -218,7 +224,7 @@ export const getGitlabCloneCommand = async (
rm -rf ${outputPath}; rm -rf ${outputPath};
mkdir -p ${outputPath}; mkdir -p ${outputPath};
if ! git clone --branch ${gitlabBranch} --depth 1 --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then 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; exit 1;
fi fi
echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath}; echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};

View File

@@ -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 { try {
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`); 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)) { if (fs.existsSync(configPath)) {
await fs.promises.unlink(configPath); await fs.promises.unlink(configPath);
} }

View File

@@ -5,6 +5,7 @@ import { dump, load } from "js-yaml";
import type { ApplicationNested } from "../builders"; import type { ApplicationNested } from "../builders";
import type { FileConfig } from "./file-types"; import type { FileConfig } from "./file-types";
import { execAsyncRemote } from "../process/execAsync"; import { execAsyncRemote } from "../process/execAsync";
import { writeTraefikConfigRemote } from "./application";
export const addMiddleware = (config: FileConfig, middlewareName: string) => { export const addMiddleware = (config: FileConfig, middlewareName: string) => {
if (config.http?.routers) { 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 config = loadMiddlewares<FileConfig>();
const { security, appName, redirects } = application; 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>() => { export const loadMiddlewares = <T>() => {