From c412dabc54991a5c4dba5beee4e1da1952c99caa Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Fri, 13 Sep 2024 01:03:38 -0600 Subject: [PATCH] refactor(multi-server): fix deploy on docker compose --- .../settings/servers/setup-server.tsx | 15 ++- apps/dokploy/server/api/routers/compose.ts | 12 ++- apps/dokploy/server/api/services/compose.ts | 38 ++++++-- apps/dokploy/server/api/services/docker.ts | 5 - apps/dokploy/server/utils/builders/compose.ts | 13 +-- apps/dokploy/server/utils/docker/domain.ts | 6 +- apps/dokploy/server/utils/docker/utils.ts | 6 +- .../dokploy/server/utils/process/execAsync.ts | 96 +++++++++++++------ apps/dokploy/server/utils/providers/raw.ts | 18 ++-- .../server/utils/servers/connection.ts | 21 ---- .../server/utils/servers/setup-server.ts | 2 +- .../server/wss/docker-container-logs.ts | 2 +- 12 files changed, 141 insertions(+), 93 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx index 7274869a..06c9a296 100644 --- a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx @@ -46,16 +46,15 @@ export const SetupServer = ({ serverId }: Props) => { { serverId }, { enabled: !!serverId, - refetchInterval: 1000, }, ); - const { mutateAsync } = api.server.setup.useMutation({ - // onMutate: async (variables) => { - // console.log("Running...."); - // utils.deployment.allByServer.invalidate({ serverId: variables.serverId }); - // // refetch(); - // }, + const { mutateAsync, isLoading } = api.server.setup.useMutation({ + onMutate: async (variables) => { + console.log("Running...."); + refetch(); + // refetch(); + }, }); return ( @@ -108,7 +107,7 @@ export const SetupServer = ({ serverId }: Props) => { }); }} > - + diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts index 591904bc..6cc95edc 100644 --- a/apps/dokploy/server/api/routers/compose.ts +++ b/apps/dokploy/server/api/routers/compose.ts @@ -19,7 +19,11 @@ import { randomizeComposeFile, randomizeSpecificationFile, } from "@/server/utils/docker/compose"; -import { addDomainToCompose, cloneCompose } from "@/server/utils/docker/domain"; +import { + addDomainToCompose, + cloneCompose, + cloneComposeRemote, +} from "@/server/utils/docker/domain"; import { removeComposeDirectory } from "@/server/utils/filesystem/directory"; import { templates } from "@/templates/templates"; import type { TemplatesKeys } from "@/templates/types/templates-data.type"; @@ -131,7 +135,11 @@ export const composeRouter = createTRPCRouter({ .mutation(async ({ input }) => { try { const compose = await findComposeById(input.composeId); - await cloneCompose(compose); + if (compose.serverId) { + await cloneComposeRemote(compose); + } else { + await cloneCompose(compose); + } return compose.sourceType; } catch (err) { throw new TRPCError({ diff --git a/apps/dokploy/server/api/services/compose.ts b/apps/dokploy/server/api/services/compose.ts index 73f36e10..3fce36f1 100644 --- a/apps/dokploy/server/api/services/compose.ts +++ b/apps/dokploy/server/api/services/compose.ts @@ -97,6 +97,7 @@ export const createComposeByTemplate = async ( .insert(compose) .values({ ...input, + serverId: "y91z1__c4SJbBe1TwQuaN", }) .returning() .then((value) => value[0]); @@ -241,16 +242,33 @@ export const deployCompose = async ({ command += getCreateComposeFileCommand(compose); } - Promise.resolve() - .then(() => { - return execAsyncRemote(compose.serverId, command); - }) - .then(() => { - return getBuildComposeCommand(compose, deployment.logPath); - }) - .then(() => { - console.log(" ---- done ----"); - }); + // 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); + } + + const steps = sequentialSteps(); + for await (const step of steps) { + if (step.stderr) { + console.log(step.stderr); + } + step; + } + + console.log(" ---- done ----"); } else { if (compose.sourceType === "github") { await cloneGithubRepository(compose, deployment.logPath, true); diff --git a/apps/dokploy/server/api/services/docker.ts b/apps/dokploy/server/api/services/docker.ts index 1554846a..fea9a7e2 100644 --- a/apps/dokploy/server/api/services/docker.ts +++ b/apps/dokploy/server/api/services/docker.ts @@ -1,9 +1,4 @@ -import { readSSHKey } from "@/server/utils/filesystem/ssh"; import { execAsync, execAsyncRemote } from "@/server/utils/process/execAsync"; -import { tail } from "lodash"; -import { stderr, stdout } from "node:process"; -import { Client } from "ssh2"; -import { findServerById } from "./server"; export const getContainers = async () => { try { diff --git a/apps/dokploy/server/utils/builders/compose.ts b/apps/dokploy/server/utils/builders/compose.ts index ad70b880..773f26a6 100644 --- a/apps/dokploy/server/utils/builders/compose.ts +++ b/apps/dokploy/server/utils/builders/compose.ts @@ -12,7 +12,7 @@ import { writeDomainsToCompose, writeDomainsToComposeRemote, } from "../docker/domain"; -import { prepareEnvironmentVariables } from "../docker/utils"; +import { encodeBase64, prepareEnvironmentVariables } from "../docker/utils"; import { spawnAsync } from "../process/spawnAsync"; import { execAsyncRemote } from "../process/execAsync"; @@ -106,9 +106,7 @@ docker ${command.split(" ").join(" ")} >> ${logPath} 2>&1; echo "Docker Compose Deployed: ✅" >> ${logPath}; `; - await execAsyncRemote(compose.serverId, bashCommand); - - return bashCommand; + return await execAsyncRemote(compose.serverId, bashCommand); }; const sanitizeCommand = (command: string) => { @@ -174,6 +172,7 @@ export const getCreateEnvFileCommand = (compose: ComposeNested) => { join(COMPOSE_PATH, appName, "code", "docker-compose.yml"); const envFilePath = join(dirname(composeFilePath), ".env"); + let envContent = env || ""; if (!envContent.includes("DOCKER_CONFIG")) { envContent += "\nDOCKER_CONFIG=/root/.docker/config.json"; @@ -184,8 +183,10 @@ export const getCreateEnvFileCommand = (compose: ComposeNested) => { } const envFileContent = prepareEnvironmentVariables(envContent).join("\n"); + + const encodedContent = encodeBase64(envFileContent); return ` -mkdir -p ${envFilePath}; -echo "${envFileContent}" > ${envFilePath}; +touch ${envFilePath}; +echo "${encodedContent}" | base64 -d > "${envFilePath}"; `; }; diff --git a/apps/dokploy/server/utils/docker/domain.ts b/apps/dokploy/server/utils/docker/domain.ts index 7d10b255..538b83cf 100644 --- a/apps/dokploy/server/utils/docker/domain.ts +++ b/apps/dokploy/server/utils/docker/domain.ts @@ -32,6 +32,7 @@ import type { PropertiesNetworks, } from "./types"; import { execAsyncRemote } from "../process/execAsync"; +import { encodeBase64 } from "./utils"; export const cloneCompose = async (compose: Compose) => { if (compose.sourceType === "github") { @@ -144,13 +145,14 @@ export const writeDomainsToComposeRemote = async ( try { if (compose.serverId) { const composeString = dump(composeConverted, { lineWidth: 1000 }); - return `printf '%s' '${composeString.replace(/'/g, "'\\''")}' > ${path}`; + const encodedContent = encodeBase64(composeString); + return `echo "${encodedContent}" | base64 -d > "${path}";`; } } catch (error) { throw error; } }; - +// (node:59875) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 SIGTERM listeners added to [process]. Use emitter.setMaxListeners() to increase limit export const addDomainToCompose = async ( compose: Compose, domains: Domain[], diff --git a/apps/dokploy/server/utils/docker/utils.ts b/apps/dokploy/server/utils/docker/utils.ts index 146b010c..f2ffb777 100644 --- a/apps/dokploy/server/utils/docker/utils.ts +++ b/apps/dokploy/server/utils/docker/utils.ts @@ -410,6 +410,8 @@ export const createFile = async ( throw error; } }; +export const encodeBase64 = (content: string) => + Buffer.from(content, "utf-8").toString("base64"); export const getCreateFileCommand = ( outputPath: string, @@ -422,10 +424,10 @@ export const getCreateFileCommand = ( } const directory = path.dirname(fullPath); - + const encodedContent = encodeBase64(content); return ` mkdir -p ${directory}; - echo "${content}" > ${fullPath}; + echo "${encodedContent}" | base64 -d > "${fullPath}"; `; }; diff --git a/apps/dokploy/server/utils/process/execAsync.ts b/apps/dokploy/server/utils/process/execAsync.ts index eb9dfa29..5b72ec62 100644 --- a/apps/dokploy/server/utils/process/execAsync.ts +++ b/apps/dokploy/server/utils/process/execAsync.ts @@ -1,39 +1,81 @@ import { exec } from "node:child_process"; import util from "node:util"; -import { connectSSH } from "../servers/connection"; +import { findServerById } from "@/server/api/services/server"; +import { readSSHKey } from "../filesystem/ssh"; +import { Client } from "ssh2"; export const execAsync = util.promisify(exec); export const execAsyncRemote = async ( - serverId: string, + serverId: string | null, command: string, ): Promise<{ stdout: string; stderr: string }> => { - const client = await connectSSH(serverId); + if (!serverId) return { stdout: "", stderr: "" }; + const server = await findServerById(serverId); + if (!server.sshKeyId) throw new Error("No SSH key available for this server"); + const keys = await readSSHKey(server.sshKeyId); + + const conn = new Client(); + let stdout = ""; + let stderr = ""; return new Promise((resolve, reject) => { - client.exec(command, (err, stream) => { - if (err) { - client.end(); - return reject(err); - } - - let stdout = ""; - let stderr = ""; - - stream - .on("data", (data: string) => { - stdout += data.toString(); - }) - .on("close", (code, signal) => { - client.end(); - if (code === 0) { - resolve({ stdout, stderr }); - } else { - reject(new Error(`Command exited with code ${code}`)); - } - }) - .stderr.on("data", (data) => { - stderr += data.toString(); + conn + .once("ready", () => { + console.log("Client :: ready"); + conn.exec(command, (err, stream) => { + if (err) throw err; + stream + .on("close", (code, signal) => { + console.log( + `Stream :: close :: code: ${code}, signal: ${signal}`, + ); + conn.end(); + if (code === 0) { + resolve({ stdout, stderr }); + } else { + reject(new Error(`Command exited with code ${code}`)); + } + }) + .on("data", (data: string) => { + stdout += data.toString(); + }) + .stderr.on("data", (data) => { + stderr += data.toString(); + }); }); - }); + }) + .connect({ + host: server.ipAddress, + port: server.port, + username: server.username, + privateKey: keys.privateKey, + timeout: 99999, + }); + + // client.exec(command, (err, stream) => { + // if (err) { + // client.end(); + // return reject(err); + // } + + // let stdout = ""; + // let stderr = ""; + + // stream + // .on("data", (data: string) => { + // stdout += data.toString(); + // }) + // .on("close", (code, signal) => { + // client.end(); + // if (code === 0) { + // resolve({ stdout, stderr }); + // } else { + // reject(new Error(`Command exited with code ${code}`)); + // } + // }) + // .stderr.on("data", (data) => { + // stderr += data.toString(); + // }); + // }); }); }; diff --git a/apps/dokploy/server/utils/providers/raw.ts b/apps/dokploy/server/utils/providers/raw.ts index ea4253e7..ea559675 100644 --- a/apps/dokploy/server/utils/providers/raw.ts +++ b/apps/dokploy/server/utils/providers/raw.ts @@ -5,6 +5,7 @@ import type { Compose } from "@/server/api/services/compose"; import { COMPOSE_PATH } from "@/server/constants"; import { recreateDirectory } from "../filesystem/directory"; import { execAsyncRemote } from "../process/execAsync"; +import { encodeBase64 } from "../docker/utils"; export const createComposeFile = async (compose: Compose, logPath: string) => { const { appName, composeFile } = compose; @@ -32,13 +33,13 @@ export const getCreateComposeFileCommand = (compose: Compose) => { const { appName, composeFile } = compose; const outputPath = join(COMPOSE_PATH, appName, "code"); const filePath = join(outputPath, "docker-compose.yml"); - const command = []; - command.push(`rm -rf ${outputPath};`); - command.push(`mkdir -p ${outputPath};`); - command.push( - `printf '%s' '${composeFile.replace(/'/g, "'\\''")}' > ${filePath};`, - ); - return command.join("\n"); + const encodedContent = encodeBase64(composeFile); + const bashCommand = ` + rm -rf ${outputPath}; + mkdir -p ${outputPath}; + echo "${encodedContent}" | base64 -d > "${filePath}"; + `; + return bashCommand; }; export const createComposeFileRaw = async (compose: Compose) => { @@ -59,9 +60,10 @@ export const createComposeFileRawRemote = async (compose: Compose) => { const filePath = join(outputPath, "docker-compose.yml"); try { + const encodedContent = encodeBase64(composeFile); const command = ` mkdir -p ${outputPath}; - echo "${composeFile}" > ${filePath}; + echo "${encodedContent}" | base64 -d > "${filePath}"; `; await execAsyncRemote(serverId, command); } catch (error) { diff --git a/apps/dokploy/server/utils/servers/connection.ts b/apps/dokploy/server/utils/servers/connection.ts index 0402006f..846c2455 100644 --- a/apps/dokploy/server/utils/servers/connection.ts +++ b/apps/dokploy/server/utils/servers/connection.ts @@ -1,24 +1,3 @@ import { findServerById } from "@/server/api/services/server"; import { Client } from "ssh2"; import { readSSHKey } from "../filesystem/ssh"; - -export const connectSSH = async (serverId: string) => { - const server = await findServerById(serverId); - if (!server.sshKeyId) throw new Error("No SSH key available for this server"); - - const keys = await readSSHKey(server.sshKeyId); - const client = new Client(); - - return new Promise((resolve, reject) => { - client - .once("ready", () => resolve(client)) - .on("error", reject) - .connect({ - host: server.ipAddress, - port: server.port, - username: server.username, - privateKey: keys.privateKey, - timeout: 99999, - }); - }); -}; diff --git a/apps/dokploy/server/utils/servers/setup-server.ts b/apps/dokploy/server/utils/servers/setup-server.ts index bcb97d00..b2b6a570 100644 --- a/apps/dokploy/server/utils/servers/setup-server.ts +++ b/apps/dokploy/server/utils/servers/setup-server.ts @@ -67,7 +67,7 @@ const connectToServer = async (serverId: string, logPath: string) => { const keys = await readSSHKey(server.sshKeyId); return new Promise((resolve, reject) => { client - .on("ready", () => { + .once("ready", () => { console.log("Client :: ready"); const bashCommand = ` diff --git a/apps/dokploy/server/wss/docker-container-logs.ts b/apps/dokploy/server/wss/docker-container-logs.ts index e0c8fba6..39876bf2 100644 --- a/apps/dokploy/server/wss/docker-container-logs.ts +++ b/apps/dokploy/server/wss/docker-container-logs.ts @@ -54,7 +54,7 @@ export const setupDockerContainerLogsWebSocketServer = ( const client = new Client(); new Promise((resolve, reject) => { client - .on("ready", () => { + .once("ready", () => { const command = ` bash -c "docker container logs --tail ${tail} --follow ${containerId}" `;