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 },
{
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) => {
});
}}
>
<Button>Setup Server</Button>
<Button isLoading={isLoading}>Setup Server</Button>
</DialogAction>
</div>
</CardHeader>

View File

@@ -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({

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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}";
`;
};

View File

@@ -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[],

View File

@@ -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}";
`;
};

View File

@@ -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();
// });
// });
});
};

View File

@@ -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) {

View File

@@ -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<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);
return new Promise<void>((resolve, reject) => {
client
.on("ready", () => {
.once("ready", () => {
console.log("Client :: ready");
const bashCommand = `

View File

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