refactor(multi-server): fix deploy on docker compose

This commit is contained in:
Mauricio Siu
2024-09-13 01:03:38 -06:00
parent 0bd0da2ee4
commit c412dabc54
12 changed files with 141 additions and 93 deletions

View File

@@ -46,16 +46,15 @@ export const SetupServer = ({ serverId }: Props) => {
{ serverId }, { serverId },
{ {
enabled: !!serverId, enabled: !!serverId,
refetchInterval: 1000,
}, },
); );
const { mutateAsync } = api.server.setup.useMutation({ const { mutateAsync, isLoading } = api.server.setup.useMutation({
// onMutate: async (variables) => { onMutate: async (variables) => {
// console.log("Running...."); console.log("Running....");
// utils.deployment.allByServer.invalidate({ serverId: variables.serverId }); refetch();
// // refetch(); // refetch();
// }, },
}); });
return ( return (
@@ -108,7 +107,7 @@ export const SetupServer = ({ serverId }: Props) => {
}); });
}} }}
> >
<Button>Setup Server</Button> <Button isLoading={isLoading}>Setup Server</Button>
</DialogAction> </DialogAction>
</div> </div>
</CardHeader> </CardHeader>

View File

@@ -19,7 +19,11 @@ import {
randomizeComposeFile, randomizeComposeFile,
randomizeSpecificationFile, randomizeSpecificationFile,
} from "@/server/utils/docker/compose"; } 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 { removeComposeDirectory } from "@/server/utils/filesystem/directory";
import { templates } from "@/templates/templates"; import { templates } from "@/templates/templates";
import type { TemplatesKeys } from "@/templates/types/templates-data.type"; import type { TemplatesKeys } from "@/templates/types/templates-data.type";
@@ -131,7 +135,11 @@ export const composeRouter = createTRPCRouter({
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
const compose = await findComposeById(input.composeId); const compose = await findComposeById(input.composeId);
await cloneCompose(compose); if (compose.serverId) {
await cloneComposeRemote(compose);
} else {
await cloneCompose(compose);
}
return compose.sourceType; return compose.sourceType;
} catch (err) { } catch (err) {
throw new TRPCError({ throw new TRPCError({

View File

@@ -97,6 +97,7 @@ export const createComposeByTemplate = async (
.insert(compose) .insert(compose)
.values({ .values({
...input, ...input,
serverId: "y91z1__c4SJbBe1TwQuaN",
}) })
.returning() .returning()
.then((value) => value[0]); .then((value) => value[0]);
@@ -241,16 +242,33 @@ export const deployCompose = async ({
command += getCreateComposeFileCommand(compose); command += getCreateComposeFileCommand(compose);
} }
Promise.resolve() // Promise.resolve()
.then(() => { // .then(() => {
return execAsyncRemote(compose.serverId, command); // return execAsyncRemote(compose.serverId, command);
}) // })
.then(() => { // .then(() => {
return getBuildComposeCommand(compose, deployment.logPath); // return getBuildComposeCommand(compose, deployment.logPath);
}) // })
.then(() => { // .catch((err) => {
console.log(" ---- done ----"); // 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 { } else {
if (compose.sourceType === "github") { if (compose.sourceType === "github") {
await cloneGithubRepository(compose, deployment.logPath, true); await cloneGithubRepository(compose, deployment.logPath, true);

View File

@@ -1,9 +1,4 @@
import { readSSHKey } from "@/server/utils/filesystem/ssh";
import { execAsync, execAsyncRemote } from "@/server/utils/process/execAsync"; 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 () => { export const getContainers = async () => {
try { try {

View File

@@ -12,7 +12,7 @@ import {
writeDomainsToCompose, writeDomainsToCompose,
writeDomainsToComposeRemote, writeDomainsToComposeRemote,
} from "../docker/domain"; } from "../docker/domain";
import { prepareEnvironmentVariables } from "../docker/utils"; import { encodeBase64, prepareEnvironmentVariables } from "../docker/utils";
import { spawnAsync } from "../process/spawnAsync"; import { spawnAsync } from "../process/spawnAsync";
import { execAsyncRemote } from "../process/execAsync"; import { execAsyncRemote } from "../process/execAsync";
@@ -106,9 +106,7 @@ docker ${command.split(" ").join(" ")} >> ${logPath} 2>&1;
echo "Docker Compose Deployed: ✅" >> ${logPath}; echo "Docker Compose Deployed: ✅" >> ${logPath};
`; `;
await execAsyncRemote(compose.serverId, bashCommand); return await execAsyncRemote(compose.serverId, bashCommand);
return bashCommand;
}; };
const sanitizeCommand = (command: string) => { const sanitizeCommand = (command: string) => {
@@ -174,6 +172,7 @@ export const getCreateEnvFileCommand = (compose: ComposeNested) => {
join(COMPOSE_PATH, appName, "code", "docker-compose.yml"); join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
const envFilePath = join(dirname(composeFilePath), ".env"); const envFilePath = join(dirname(composeFilePath), ".env");
let envContent = env || ""; let envContent = env || "";
if (!envContent.includes("DOCKER_CONFIG")) { if (!envContent.includes("DOCKER_CONFIG")) {
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json"; envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
@@ -184,8 +183,10 @@ export const getCreateEnvFileCommand = (compose: ComposeNested) => {
} }
const envFileContent = prepareEnvironmentVariables(envContent).join("\n"); const envFileContent = prepareEnvironmentVariables(envContent).join("\n");
const encodedContent = encodeBase64(envFileContent);
return ` return `
mkdir -p ${envFilePath}; touch ${envFilePath};
echo "${envFileContent}" > ${envFilePath}; echo "${encodedContent}" | base64 -d > "${envFilePath}";
`; `;
}; };

View File

@@ -32,6 +32,7 @@ import type {
PropertiesNetworks, PropertiesNetworks,
} from "./types"; } from "./types";
import { execAsyncRemote } from "../process/execAsync"; import { execAsyncRemote } from "../process/execAsync";
import { encodeBase64 } from "./utils";
export const cloneCompose = async (compose: Compose) => { export const cloneCompose = async (compose: Compose) => {
if (compose.sourceType === "github") { if (compose.sourceType === "github") {
@@ -144,13 +145,14 @@ export const writeDomainsToComposeRemote = async (
try { try {
if (compose.serverId) { if (compose.serverId) {
const composeString = dump(composeConverted, { lineWidth: 1000 }); 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) { } catch (error) {
throw 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 ( export const addDomainToCompose = async (
compose: Compose, compose: Compose,
domains: Domain[], domains: Domain[],

View File

@@ -410,6 +410,8 @@ export const createFile = async (
throw error; throw error;
} }
}; };
export const encodeBase64 = (content: string) =>
Buffer.from(content, "utf-8").toString("base64");
export const getCreateFileCommand = ( export const getCreateFileCommand = (
outputPath: string, outputPath: string,
@@ -422,10 +424,10 @@ export const getCreateFileCommand = (
} }
const directory = path.dirname(fullPath); const directory = path.dirname(fullPath);
const encodedContent = encodeBase64(content);
return ` return `
mkdir -p ${directory}; mkdir -p ${directory};
echo "${content}" > ${fullPath}; echo "${encodedContent}" | base64 -d > "${fullPath}";
`; `;
}; };

View File

@@ -1,39 +1,81 @@
import { exec } from "node:child_process"; import { exec } from "node:child_process";
import util from "node:util"; 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 execAsync = util.promisify(exec);
export const execAsyncRemote = async ( export const execAsyncRemote = async (
serverId: string, serverId: string | null,
command: string, command: string,
): Promise<{ stdout: string; stderr: 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) => { return new Promise((resolve, reject) => {
client.exec(command, (err, stream) => { conn
if (err) { .once("ready", () => {
client.end(); console.log("Client :: ready");
return reject(err); conn.exec(command, (err, stream) => {
} if (err) throw err;
stream
let stdout = ""; .on("close", (code, signal) => {
let stderr = ""; console.log(
`Stream :: close :: code: ${code}, signal: ${signal}`,
stream );
.on("data", (data: string) => { conn.end();
stdout += data.toString(); if (code === 0) {
}) resolve({ stdout, stderr });
.on("close", (code, signal) => { } else {
client.end(); reject(new Error(`Command exited with code ${code}`));
if (code === 0) { }
resolve({ stdout, stderr }); })
} else { .on("data", (data: string) => {
reject(new Error(`Command exited with code ${code}`)); stdout += data.toString();
} })
}) .stderr.on("data", (data) => {
.stderr.on("data", (data) => { stderr += data.toString();
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();
// });
// });
}); });
}; };

View File

@@ -5,6 +5,7 @@ import type { Compose } from "@/server/api/services/compose";
import { COMPOSE_PATH } from "@/server/constants"; import { COMPOSE_PATH } from "@/server/constants";
import { recreateDirectory } from "../filesystem/directory"; import { recreateDirectory } from "../filesystem/directory";
import { execAsyncRemote } from "../process/execAsync"; import { execAsyncRemote } from "../process/execAsync";
import { encodeBase64 } from "../docker/utils";
export const createComposeFile = async (compose: Compose, logPath: string) => { export const createComposeFile = async (compose: Compose, logPath: string) => {
const { appName, composeFile } = compose; const { appName, composeFile } = compose;
@@ -32,13 +33,13 @@ export const getCreateComposeFileCommand = (compose: Compose) => {
const { appName, composeFile } = compose; const { appName, composeFile } = compose;
const outputPath = join(COMPOSE_PATH, appName, "code"); const outputPath = join(COMPOSE_PATH, appName, "code");
const filePath = join(outputPath, "docker-compose.yml"); const filePath = join(outputPath, "docker-compose.yml");
const command = []; const encodedContent = encodeBase64(composeFile);
command.push(`rm -rf ${outputPath};`); const bashCommand = `
command.push(`mkdir -p ${outputPath};`); rm -rf ${outputPath};
command.push( mkdir -p ${outputPath};
`printf '%s' '${composeFile.replace(/'/g, "'\\''")}' > ${filePath};`, echo "${encodedContent}" | base64 -d > "${filePath}";
); `;
return command.join("\n"); return bashCommand;
}; };
export const createComposeFileRaw = async (compose: Compose) => { export const createComposeFileRaw = async (compose: Compose) => {
@@ -59,9 +60,10 @@ export const createComposeFileRawRemote = async (compose: Compose) => {
const filePath = join(outputPath, "docker-compose.yml"); const filePath = join(outputPath, "docker-compose.yml");
try { try {
const encodedContent = encodeBase64(composeFile);
const command = ` const command = `
mkdir -p ${outputPath}; mkdir -p ${outputPath};
echo "${composeFile}" > ${filePath}; echo "${encodedContent}" | base64 -d > "${filePath}";
`; `;
await execAsyncRemote(serverId, command); await execAsyncRemote(serverId, command);
} catch (error) { } catch (error) {

View File

@@ -1,24 +1,3 @@
import { findServerById } from "@/server/api/services/server"; import { findServerById } from "@/server/api/services/server";
import { Client } from "ssh2"; import { Client } from "ssh2";
import { readSSHKey } from "../filesystem/ssh"; 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<Client>((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,
});
});
};

View File

@@ -67,7 +67,7 @@ const connectToServer = async (serverId: string, logPath: string) => {
const keys = await readSSHKey(server.sshKeyId); const keys = await readSSHKey(server.sshKeyId);
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
client client
.on("ready", () => { .once("ready", () => {
console.log("Client :: ready"); console.log("Client :: ready");
const bashCommand = ` const bashCommand = `

View File

@@ -54,7 +54,7 @@ export const setupDockerContainerLogsWebSocketServer = (
const client = new Client(); const client = new Client();
new Promise<void>((resolve, reject) => { new Promise<void>((resolve, reject) => {
client client
.on("ready", () => { .once("ready", () => {
const command = ` const command = `
bash -c "docker container logs --tail ${tail} --follow ${containerId}" bash -c "docker container logs --tail ${tail} --follow ${containerId}"
`; `;