Connect to remote Dokku server using SSH.
This commit is contained in:
37
backend/server/src/DokkuClient.ts
Normal file
37
backend/server/src/DokkuClient.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { SSHSocketClient, SSHConfig } from "./SSHSocketClient"
|
||||
|
||||
export interface DokkuResponse {
|
||||
ok: boolean;
|
||||
output: string;
|
||||
}
|
||||
|
||||
export class DokkuClient extends SSHSocketClient {
|
||||
|
||||
constructor(config: SSHConfig) {
|
||||
super(
|
||||
config,
|
||||
"/var/run/dokku-daemon/dokku-daemon.sock"
|
||||
)
|
||||
}
|
||||
|
||||
async sendCommand(command: string): Promise<DokkuResponse> {
|
||||
try {
|
||||
const response = await this.sendData(command);
|
||||
|
||||
if (typeof response !== "string") {
|
||||
throw new Error("Received data is not a string");
|
||||
}
|
||||
|
||||
return JSON.parse(response);
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to send command: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async listApps(): Promise<string[]> {
|
||||
const response = await this.sendCommand("apps:list");
|
||||
return response.output.split("\n").slice(1); // Split by newline and ignore the first line (header)
|
||||
}
|
||||
}
|
||||
|
||||
export { SSHConfig };
|
90
backend/server/src/SSHSocketClient.ts
Normal file
90
backend/server/src/SSHSocketClient.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { Client } from "ssh2";
|
||||
|
||||
export interface SSHConfig {
|
||||
host: string;
|
||||
port?: number;
|
||||
username: string;
|
||||
privateKey: Buffer;
|
||||
}
|
||||
|
||||
export class SSHSocketClient {
|
||||
private conn: Client;
|
||||
private config: SSHConfig;
|
||||
private socketPath: string;
|
||||
private isConnected: boolean = false;
|
||||
|
||||
constructor(config: SSHConfig, socketPath: string) {
|
||||
this.conn = new Client();
|
||||
this.config = { ...config, port: 22};
|
||||
this.socketPath = socketPath;
|
||||
|
||||
this.setupTerminationHandlers();
|
||||
}
|
||||
|
||||
private setupTerminationHandlers() {
|
||||
process.on("SIGINT", this.closeConnection.bind(this));
|
||||
process.on("SIGTERM", this.closeConnection.bind(this));
|
||||
}
|
||||
|
||||
private closeConnection() {
|
||||
console.log("Closing SSH connection...");
|
||||
this.conn.end();
|
||||
this.isConnected = false;
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
connect(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.conn
|
||||
.on("ready", () => {
|
||||
console.log("SSH connection established");
|
||||
this.isConnected = true;
|
||||
resolve();
|
||||
})
|
||||
.on("error", (err) => {
|
||||
console.error("SSH connection error:", err);
|
||||
this.isConnected = false;
|
||||
reject(err);
|
||||
})
|
||||
.on("close", () => {
|
||||
console.log("SSH connection closed");
|
||||
this.isConnected = false;
|
||||
})
|
||||
.connect(this.config);
|
||||
});
|
||||
}
|
||||
|
||||
sendData(data: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.isConnected) {
|
||||
reject(new Error("SSH connection is not established"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.conn.exec(
|
||||
`echo "${data}" | nc -U ${this.socketPath}`,
|
||||
(err, stream) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
stream
|
||||
.on("close", (code: number, signal: string) => {
|
||||
reject(
|
||||
new Error(
|
||||
`Stream closed with code ${code} and signal ${signal}`
|
||||
)
|
||||
);
|
||||
})
|
||||
.on("data", (data: Buffer) => {
|
||||
resolve(data.toString());
|
||||
})
|
||||
.stderr.on("data", (data: Buffer) => {
|
||||
reject(new Error(data.toString()));
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
@ -5,7 +5,8 @@ import express, { Express } from "express";
|
||||
import dotenv from "dotenv";
|
||||
import { createServer } from "http";
|
||||
import { Server } from "socket.io";
|
||||
import net from "net";
|
||||
import { DokkuClient, SSHConfig } from "./DokkuClient";
|
||||
import fs from "fs";
|
||||
|
||||
import { z } from "zod";
|
||||
import { User } from "./types";
|
||||
@ -113,20 +114,17 @@ io.use(async (socket, next) => {
|
||||
|
||||
const lockManager = new LockManager();
|
||||
|
||||
const client = net.createConnection(
|
||||
{ path: "/var/run/remote-dokku.sock" },
|
||||
() => {
|
||||
console.log("Connected to Dokku server");
|
||||
}
|
||||
);
|
||||
if (!process.env.DOKKU_HOST) throw new Error('Environment variable DOKKU_HOST is not defined');
|
||||
if (!process.env.DOKKU_USERNAME) throw new Error('Environment variable DOKKU_USERNAME is not defined');
|
||||
if (!process.env.DOKKU_KEY) throw new Error('Environment variable DOKKU_KEY is not defined');
|
||||
|
||||
client.on("end", () => {
|
||||
console.log("Disconnected from Dokku server");
|
||||
const client = new DokkuClient({
|
||||
host: process.env.DOKKU_HOST,
|
||||
username: process.env.DOKKU_USERNAME,
|
||||
privateKey: fs.readFileSync(process.env.DOKKU_KEY),
|
||||
});
|
||||
|
||||
client.on("error", (err) => {
|
||||
console.error(`Dokku Client error: ${err}`);
|
||||
});
|
||||
client.connect();
|
||||
|
||||
io.on("connection", async (socket) => {
|
||||
try {
|
||||
@ -270,11 +268,6 @@ io.on("connection", async (socket) => {
|
||||
}
|
||||
);
|
||||
|
||||
interface DokkuResponse {
|
||||
ok: boolean;
|
||||
output: string;
|
||||
}
|
||||
|
||||
interface CallbackResponse {
|
||||
success: boolean;
|
||||
apps?: string[];
|
||||
@ -285,20 +278,17 @@ io.on("connection", async (socket) => {
|
||||
"list",
|
||||
async (callback: (response: CallbackResponse) => void) => {
|
||||
console.log("Retrieving apps list...");
|
||||
client.on("data", (data) => {
|
||||
const response = data.toString();
|
||||
const parsedData: DokkuResponse = JSON.parse(response);
|
||||
if (parsedData.ok) {
|
||||
const appsList = parsedData.output.split("\n").slice(1); // Split by newline and ignore the first line (header)
|
||||
callback({ success: true, apps: appsList });
|
||||
} else {
|
||||
callback({
|
||||
success: false,
|
||||
message: "Failed to retrieve apps list",
|
||||
});
|
||||
}
|
||||
});
|
||||
client.write("apps:list\n");
|
||||
try {
|
||||
callback({
|
||||
success: true,
|
||||
apps: await client.listApps()
|
||||
});
|
||||
} catch (error) {
|
||||
callback({
|
||||
success: false,
|
||||
message: "Failed to retrieve apps list",
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
Reference in New Issue
Block a user