Connect to remote Dokku server using SSH.

This commit is contained in:
James Murdza
2024-07-21 14:18:14 -04:00
parent 769f52816f
commit de4923ec1e
5 changed files with 237 additions and 31 deletions

View 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 };

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

View File

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