diff --git a/backend/agent-socket-handler.ts b/backend/agent-socket-handler.ts new file mode 100644 index 0000000..ab418ae --- /dev/null +++ b/backend/agent-socket-handler.ts @@ -0,0 +1,7 @@ +import { DockgeServer } from "./dockge-server"; +import { AgentSocket } from "../../common/agent-socket"; +import { DockgeSocket } from "./util-server"; + +export abstract class AgentSocketHandler { + abstract create(socket : DockgeSocket, server : DockgeServer, agentSocket : AgentSocket): void; +} diff --git a/backend/socket-handlers/docker-socket-handler.ts b/backend/agent-socket-handlers/docker-socket-handler.ts similarity index 78% rename from backend/socket-handlers/docker-socket-handler.ts rename to backend/agent-socket-handlers/docker-socket-handler.ts index 036d171..a53e284 100644 --- a/backend/socket-handlers/docker-socket-handler.ts +++ b/backend/agent-socket-handlers/docker-socket-handler.ts @@ -1,17 +1,16 @@ -import { SocketHandler } from "../socket-handler.js"; +import { AgentSocketHandler } from "../agent-socket-handler"; import { DockgeServer } from "../dockge-server"; import { callbackError, checkLogin, DockgeSocket, ValidationError } from "../util-server"; import { Stack } from "../stack"; +import { AgentSocket } from "../../common/agent-socket"; -// @ts-ignore -import composerize from "composerize"; +export class DockerSocketHandler extends AgentSocketHandler { + create(s : DockgeSocket, server : DockgeServer, agentSocket : AgentSocket) { + // Do not call super.create() -export class DockerSocketHandler extends SocketHandler { - create(socket : DockgeSocket, server : DockgeServer) { - - socket.on("deployStack", async (name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown, callback) => { + agentSocket.on("deployStack", async (name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown, callback) => { try { - checkLogin(socket); + checkLogin(s); const stack = await this.saveStack(socket, server, name, composeYAML, composeENV, isAdd); await stack.deploy(socket); server.sendStackList(); @@ -25,7 +24,7 @@ export class DockerSocketHandler extends SocketHandler { } }); - socket.on("saveStack", async (name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown, callback) => { + agentSocket.on("saveStack", async (name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown, callback) => { try { checkLogin(socket); this.saveStack(socket, server, name, composeYAML, composeENV, isAdd); @@ -39,7 +38,7 @@ export class DockerSocketHandler extends SocketHandler { } }); - socket.on("deleteStack", async (name : unknown, callback) => { + agentSocket.on("deleteStack", async (name : unknown, callback) => { try { checkLogin(socket); if (typeof(name) !== "string") { @@ -65,9 +64,9 @@ export class DockerSocketHandler extends SocketHandler { } }); - socket.on("getStack", async (stackName : unknown, callback) => { + agentSocket.on("getStack", async (stackName : unknown, callback) => { try { - checkLogin(socket); + checkLogin(s); if (typeof(stackName) !== "string") { throw new ValidationError("Stack name must be a string"); @@ -76,7 +75,7 @@ export class DockerSocketHandler extends SocketHandler { const stack = await Stack.getStack(server, stackName); if (stack.isManagedByDockge) { - stack.joinCombinedTerminal(socket); + stack.joinCombinedTerminal(s); } callback({ @@ -89,7 +88,7 @@ export class DockerSocketHandler extends SocketHandler { }); // requestStackList - socket.on("requestStackList", async (callback) => { + agentSocket.on("requestStackList", async (callback) => { try { checkLogin(socket); server.sendStackList(); @@ -103,7 +102,7 @@ export class DockerSocketHandler extends SocketHandler { }); // startStack - socket.on("startStack", async (stackName : unknown, callback) => { + agentSocket.on("startStack", async (stackName : unknown, callback) => { try { checkLogin(socket); @@ -127,7 +126,7 @@ export class DockerSocketHandler extends SocketHandler { }); // stopStack - socket.on("stopStack", async (stackName : unknown, callback) => { + agentSocket.on("stopStack", async (stackName : unknown, callback) => { try { checkLogin(socket); @@ -148,16 +147,16 @@ export class DockerSocketHandler extends SocketHandler { }); // restartStack - socket.on("restartStack", async (stackName : unknown, callback) => { + agentSocket.on("restartStack", async (stackName : unknown, callback) => { try { - checkLogin(socket); + checkLogin(s); if (typeof(stackName) !== "string") { throw new ValidationError("Stack name must be a string"); } const stack = await Stack.getStack(server, stackName); - await stack.restart(socket); + await stack.restart(s); callback({ ok: true, msg: "Restarted" @@ -169,7 +168,7 @@ export class DockerSocketHandler extends SocketHandler { }); // updateStack - socket.on("updateStack", async (stackName : unknown, callback) => { + agentSocket.on("updateStack", async (stackName : unknown, callback) => { try { checkLogin(socket); @@ -190,7 +189,7 @@ export class DockerSocketHandler extends SocketHandler { }); // down stack - socket.on("downStack", async (stackName : unknown, callback) => { + agentSocket.on("downStack", async (stackName : unknown, callback) => { try { checkLogin(socket); @@ -211,9 +210,9 @@ export class DockerSocketHandler extends SocketHandler { }); // Services status - socket.on("serviceStatusList", async (stackName : unknown, callback) => { + agentSocket.on("serviceStatusList", async (stackName : unknown, callback) => { try { - checkLogin(socket); + checkLogin(s); if (typeof(stackName) !== "string") { throw new ValidationError("Stack name must be a string"); @@ -231,7 +230,7 @@ export class DockerSocketHandler extends SocketHandler { }); // getExternalNetworkList - socket.on("getDockerNetworkList", async (callback) => { + agentSocket.on("getDockerNetworkList", async (callback) => { try { checkLogin(socket); const dockerNetworkList = await server.getDockerNetworkList(); @@ -243,25 +242,6 @@ export class DockerSocketHandler extends SocketHandler { callbackError(e, callback); } }); - - // composerize - socket.on("composerize", async (dockerRunCommand : unknown, callback) => { - try { - checkLogin(socket); - - if (typeof(dockerRunCommand) !== "string") { - throw new ValidationError("dockerRunCommand must be a string"); - } - - const composeTemplate = composerize(dockerRunCommand); - callback({ - ok: true, - composeTemplate, - }); - } catch (e) { - callbackError(e, callback); - } - }); } async saveStack(socket : DockgeSocket, server : DockgeServer, name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown) : Promise { diff --git a/backend/database.ts b/backend/database.ts index a63bc18..0daa0db 100644 --- a/backend/database.ts +++ b/backend/database.ts @@ -9,7 +9,7 @@ import knex from "knex"; import Dialect from "knex/lib/dialects/sqlite3/index.js"; import sqlite from "@louislam/sqlite3"; -import { sleep } from "./util-common"; +import { sleep } from "../common/util-common"; interface DBConfig { type?: "sqlite" | "mysql"; diff --git a/backend/dockge-instance-manager.ts b/backend/dockge-instance-manager.ts new file mode 100644 index 0000000..6de7c1d --- /dev/null +++ b/backend/dockge-instance-manager.ts @@ -0,0 +1,114 @@ +import { DockgeSocket } from "./util-server"; +import { io, Socket as SocketClient } from "socket.io-client"; +import { log } from "./log"; + +/** + * Dockge Instance Manager + */ +export class DockgeInstanceManager { + + protected socket : DockgeSocket; + protected instanceSocketList : Record = {}; + + constructor(socket: DockgeSocket) { + this.socket = socket; + } + + connect(endpoint : string, tls : boolean, username : string, password : string) { + if (this.instanceSocketList[endpoint]) { + log.debug("INSTANCEMANAGER", "Already connected to the socket server: " + endpoint); + return; + } + + let url = ((tls) ? "wss://" : "ws://") + endpoint; + + log.info("INSTANCEMANAGER", "Connecting to the socket server: " + endpoint); + let client = io(url, { + transports: [ "websocket", "polling" ], + extraHeaders: { + endpoint, + } + }); + + client.on("connect", () => { + log.info("INSTANCEMANAGER", "Connected to the socket server: " + endpoint); + + client.emit("login", { + username: username, + password: password, + }, (res) => { + if (res.ok) { + log.info("INSTANCEMANAGER", "Logged in to the socket server: " + endpoint); + } else { + log.error("INSTANCEMANAGER", "Failed to login to the socket server: " + endpoint); + } + }); + }); + + client.on("error", (err) => { + log.error("INSTANCEMANAGER", "Error from the socket server: " + endpoint); + log.error("INSTANCEMANAGER", err); + }); + + client.on("disconnect", () => { + log.info("INSTANCEMANAGER", "Disconnected from the socket server: " + endpoint); + }); + + client.on("agent", (...args : unknown[]) => { + log.debug("INSTANCEMANAGER", "Forward event"); + this.socket.emit("agent", ...args); + }); + + this.instanceSocketList[endpoint] = client; + } + + disconnect(endpoint : string) { + let client = this.instanceSocketList[endpoint]; + client?.disconnect(); + } + + connectAll() { + if (this.socket.endpoint) { + log.info("INSTANCEMANAGER", "This connection is connected as an agent, skip connectAll()"); + return; + } + + let list : Record = { + + }; + + if (process.env.DOCKGE_TEST_REMOTE_HOST) { + list[process.env.DOCKGE_TEST_REMOTE_HOST] = { + tls: false, + username: "admin", + password: process.env.DOCKGE_TEST_REMOTE_PW || "", + }; + } + + if (Object.keys(list).length !== 0) { + log.info("INSTANCEMANAGER", "Connecting to all instance socket server(s)..."); + } + + for (let endpoint in list) { + let item = list[endpoint]; + this.connect(endpoint, item.tls, item.username, item.password); + } + } + + disconnectAll() { + for (let endpoint in this.instanceSocketList) { + this.disconnect(endpoint); + } + } + + emitToEndpoint(endpoint: string, eventName: string, ...args : unknown[]) { + log.debug("INSTANCEMANAGER", "Emitting event to endpoint: " + endpoint); + let client = this.instanceSocketList[endpoint]; + if (!client) { + log.error("INSTANCEMANAGER", "Socket client not found for endpoint: " + endpoint); + throw new Error("Socket client not found for endpoint: " + endpoint); + } + client?.emit("agent", endpoint, eventName, ...args); + } + +} diff --git a/backend/dockge-server.ts b/backend/dockge-server.ts index a55ecc3..1035ade 100644 --- a/backend/dockge-server.ts +++ b/backend/dockge-server.ts @@ -1,3 +1,4 @@ +import "dotenv/config"; import { MainRouter } from "./routers/main-router"; import * as fs from "node:fs"; import { PackageJson } from "type-fest"; @@ -17,11 +18,11 @@ import { Settings } from "./settings"; import checkVersion from "./check-version"; import dayjs from "dayjs"; import { R } from "redbean-node"; -import { genSecret, isDev } from "./util-common"; +import { genSecret, isDev, LooseObject } from "../common/util-common"; import { generatePasswordHash } from "./password-hash"; import { Bean } from "redbean-node/dist/bean"; import { Arguments, Config, DockgeSocket } from "./util-server"; -import { DockerSocketHandler } from "./socket-handlers/docker-socket-handler"; +import { DockerSocketHandler } from "./agent-socket-handlers/docker-socket-handler"; import expressStaticGzip from "express-static-gzip"; import path from "path"; import { TerminalSocketHandler } from "./socket-handlers/terminal-socket-handler"; @@ -30,9 +31,10 @@ import { Cron } from "croner"; import gracefulShutdown from "http-graceful-shutdown"; import User from "./models/user"; import childProcessAsync from "promisify-child-process"; -import { Terminal } from "./terminal"; - -import "dotenv/config"; +import { DockgeInstanceManager } from "./dockge-instance-manager"; +import { AgentProxySocketHandler } from "./socket-handlers/agent-proxy-socket-handler"; +import { AgentSocketHandler } from "./agent-socket-handler"; +import { AgentSocket } from "../common/agent-socket"; export class DockgeServer { app : Express; @@ -54,10 +56,15 @@ export class DockgeServer { */ socketHandlerList : SocketHandler[] = [ new MainSocketHandler(), - new DockerSocketHandler(), new TerminalSocketHandler(), ]; + agentProxySocketHandler = new AgentProxySocketHandler(); + + agentSocketHandlerList : AgentSocketHandler[] = [ + new DockerSocketHandler(), + ]; + /** * Show Setup Page */ @@ -230,20 +237,52 @@ export class DockgeServer { }); this.io.on("connection", async (socket: Socket) => { - log.info("server", "Socket connected!"); + let dockgeSocket = socket as DockgeSocket; + dockgeSocket.instanceManager = new DockgeInstanceManager(dockgeSocket); + dockgeSocket.emitAgent = (event : string, ...args : unknown[]) => { + let obj = args[0]; + if (typeof(obj) === "object") { + let obj2 = obj as LooseObject; + obj2.endpoint = dockgeSocket.endpoint; + } + dockgeSocket.emit("agent", event, ...args); + }; - this.sendInfo(socket, true); + if (typeof(socket.request.headers.endpoint) === "string") { + dockgeSocket.endpoint = socket.request.headers.endpoint; + } else { + dockgeSocket.endpoint = ""; + } + + if (dockgeSocket.endpoint) { + log.info("server", "Socket connected (agent), as endpoint " + dockgeSocket.endpoint); + } else { + log.info("server", "Socket connected (direct)"); + } + + this.sendInfo(dockgeSocket, true); if (this.needSetup) { log.info("server", "Redirect to setup page"); - socket.emit("setup"); + dockgeSocket.emit("setup"); } - // Create socket handlers + // Create socket handlers (original, no agent support) for (const socketHandler of this.socketHandlerList) { - socketHandler.create(socket as DockgeSocket, this); + socketHandler.create(dockgeSocket, this); } + // Create Agent Socket + let agentSocket = new AgentSocket(); + + // Create agent socket handlers + for (const socketHandler of this.agentSocketHandlerList) { + socketHandler.create(dockgeSocket, this, agentSocket); + } + + // Create agent proxy socket handlers + this.agentProxySocketHandler.create2(dockgeSocket, this, agentSocket); + // *************************** // Better do anything after added all socket handlers here // *************************** @@ -251,12 +290,18 @@ export class DockgeServer { log.debug("auth", "check auto login"); if (await Settings.get("disableAuth")) { log.info("auth", "Disabled Auth: auto login to admin"); - this.afterLogin(socket as DockgeSocket, await R.findOne("user") as User); - socket.emit("autoLogin"); + this.afterLogin(dockgeSocket, await R.findOne("user") as User); + dockgeSocket.emit("autoLogin"); } else { log.debug("auth", "need auth"); } + // Socket disconnect + dockgeSocket.on("disconnect", () => { + log.info("server", "Socket disconnected!"); + dockgeSocket.instanceManager.disconnectAll(); + }); + }); this.io.on("disconnect", () => { @@ -265,7 +310,7 @@ export class DockgeServer { if (isDev) { setInterval(() => { - log.debug("terminal", "Terminal count: " + Terminal.getTerminalCount()); + //log.debug("terminal", "Terminal count: " + Terminal.getTerminalCount()); }, 5000); } } @@ -281,6 +326,9 @@ export class DockgeServer { } catch (e) { log.error("server", e); } + + // Also connect to other dockge instances + socket.instanceManager.connectAll(); } /** @@ -519,26 +567,34 @@ export class DockgeServer { return jwtSecretBean; } + /** + * Send stack list to all connected sockets + * @param useCache + */ async sendStackList(useCache = false) { - let roomList = this.io.sockets.adapter.rooms.keys(); - let map : Map | undefined; + let socketList = this.io.sockets.sockets.values(); + + let stackList; + + for (let socket of socketList) { + let dockgeSocket = socket as DockgeSocket; - for (let room of roomList) { // Check if the room is a number (user id) - if (Number(room)) { + if (dockgeSocket.userID) { - // Get the list only if there is a room - if (!map) { - map = new Map(); - let stackList = await Stack.getStackList(this, useCache); - - for (let [ stackName, stack ] of stackList) { - map.set(stackName, stack.toSimpleJSON()); - } + // Get the list only if there is a logged in user + if (!stackList) { + stackList = await Stack.getStackList(this, useCache); } - log.debug("server", "Send stack list to room " + room); - this.io.to(room).emit("stackList", { + let map : Map = new Map(); + + for (let [ stackName, stack ] of stackList) { + map.set(stackName, stack.toSimpleJSON(dockgeSocket.endpoint)); + } + + log.debug("server", "Send stack list"); + dockgeSocket.emitAgent("stackList", { ok: true, stackList: Object.fromEntries(map), }); diff --git a/backend/log.ts b/backend/log.ts index 3cfac64..853e2a9 100644 --- a/backend/log.ts +++ b/backend/log.ts @@ -1,6 +1,6 @@ // Console colors // https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color -import { intHash, isDev } from "./util-common"; +import { intHash, isDev } from "../common/util-common"; import dayjs from "dayjs"; export const CONSOLE_STYLE_Reset = "\x1b[0m"; diff --git a/backend/settings.ts b/backend/settings.ts index c1703dc..bbc00a0 100644 --- a/backend/settings.ts +++ b/backend/settings.ts @@ -1,6 +1,6 @@ import { R } from "redbean-node"; import { log } from "./log"; -import { LooseObject } from "./util-common"; +import { LooseObject } from "../common/util-common"; export class Settings { diff --git a/backend/socket-handlers/agent-proxy-socket-handler.ts b/backend/socket-handlers/agent-proxy-socket-handler.ts new file mode 100644 index 0000000..03f0895 --- /dev/null +++ b/backend/socket-handlers/agent-proxy-socket-handler.ts @@ -0,0 +1,43 @@ +import { SocketHandler } from "../socket-handler.js"; +import { DockgeServer } from "../dockge-server"; +import { log } from "../log"; +import { checkLogin, DockgeSocket } from "../util-server"; +import { AgentSocket } from "../../common/agent-socket"; + +export class AgentProxySocketHandler extends SocketHandler { + + create2(socket : DockgeSocket, server : DockgeServer, agentSocket : AgentSocket) { + // Agent - proxying requests if needed + socket.on("agent", async (endpoint : unknown, eventName : unknown, ...args : unknown[]) => { + try { + checkLogin(socket); + + // Check Type + if (typeof(endpoint) !== "string") { + throw new Error("Endpoint must be a string"); + } + if (typeof(eventName) !== "string") { + throw new Error("Event name must be a string"); + } + + log.debug("agent", "Proxying request to " + endpoint + " for " + eventName); + + // Direct connection or matching endpoint + if (!endpoint || endpoint === socket.endpoint) { + log.debug("agent", "Direct connection"); + agentSocket.call(eventName, ...args); + } else { + socket.instanceManager.emitToEndpoint(endpoint, eventName, ...args); + } + } catch (e) { + if (e instanceof Error) { + log.warn("agent", e.message); + } + } + }); + } + + create(socket : DockgeSocket, server : DockgeServer) { + throw new Error("Method not implemented. Please use create2 instead."); + } +} diff --git a/backend/socket-handlers/main-socket-handler.ts b/backend/socket-handlers/main-socket-handler.ts index e6734d5..bc9550a 100644 --- a/backend/socket-handlers/main-socket-handler.ts +++ b/backend/socket-handlers/main-socket-handler.ts @@ -1,3 +1,5 @@ +// @ts-ignore +import composerize from "composerize"; import { SocketHandler } from "../socket-handler.js"; import { DockgeServer } from "../dockge-server"; import { log } from "../log"; @@ -5,7 +7,14 @@ import { R } from "redbean-node"; import { loginRateLimiter, twoFaRateLimiter } from "../rate-limiter"; import { generatePasswordHash, needRehashPassword, shake256, SHAKE256_LENGTH, verifyPassword } from "../password-hash"; import { User } from "../models/user"; -import { checkLogin, DockgeSocket, doubleCheckPassword, JWTDecoded } from "../util-server"; +import { + callbackError, + checkLogin, + DockgeSocket, + doubleCheckPassword, + JWTDecoded, + ValidationError +} from "../util-server"; import { passwordStrength } from "check-password-strength"; import jwt from "jsonwebtoken"; import { Settings } from "../settings"; @@ -294,6 +303,25 @@ export class MainSocketHandler extends SocketHandler { } } }); + + // composerize + socket.on("composerize", async (dockerRunCommand : unknown, callback) => { + try { + checkLogin(socket); + + if (typeof(dockerRunCommand) !== "string") { + throw new ValidationError("dockerRunCommand must be a string"); + } + + const composeTemplate = composerize(dockerRunCommand); + callback({ + ok: true, + composeTemplate, + }); + } catch (e) { + callbackError(e, callback); + } + }); } async login(username : string, password : string) : Promise { diff --git a/backend/socket-handlers/terminal-socket-handler.ts b/backend/socket-handlers/terminal-socket-handler.ts index 9ae6656..a64b170 100644 --- a/backend/socket-handlers/terminal-socket-handler.ts +++ b/backend/socket-handlers/terminal-socket-handler.ts @@ -11,7 +11,7 @@ import { getComposeTerminalName, getContainerExecTerminalName, isDev, PROGRESS_TERMINAL_ROWS -} from "../util-common"; +} from "../../common/util-common"; import { InteractiveTerminal, MainTerminal, Terminal } from "../terminal"; import { Stack } from "../stack"; diff --git a/backend/stack.ts b/backend/stack.ts index 1fbfa13..743ccd8 100644 --- a/backend/stack.ts +++ b/backend/stack.ts @@ -15,7 +15,7 @@ import { PROGRESS_TERMINAL_ROWS, RUNNING, TERMINAL_ROWS, UNKNOWN -} from "./util-common"; +} from "../common/util-common"; import { InteractiveTerminal, Terminal } from "./terminal"; import childProcessAsync from "promisify-child-process"; @@ -50,8 +50,8 @@ export class Stack { } } - toJSON() : object { - let obj = this.toSimpleJSON(); + toJSON(endpoint : string) : object { + let obj = this.toSimpleJSON(endpoint); return { ...obj, composeYAML: this.composeYAML, @@ -59,13 +59,14 @@ export class Stack { }; } - toSimpleJSON() : object { + toSimpleJSON(endpoint : string) : object { return { name: this.name, status: this._status, tags: [], isManagedByDockge: this.isManagedByDockge, composeFileName: this._composeFileName, + endpoint, }; } @@ -186,8 +187,8 @@ export class Stack { } } - async deploy(socket? : DockgeSocket) : Promise { - const terminalName = getComposeTerminalName(this.name); + async deploy(socket : DockgeSocket) : Promise { + const terminalName = getComposeTerminalName(socket.endpoint, this.name); let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "up", "-d", "--remove-orphans" ], this.path); if (exitCode !== 0) { throw new Error("Failed to deploy, please check the terminal output for more information."); @@ -195,8 +196,8 @@ export class Stack { return exitCode; } - async delete(socket?: DockgeSocket) : Promise { - const terminalName = getComposeTerminalName(this.name); + async delete(socket: DockgeSocket) : Promise { + const terminalName = getComposeTerminalName(socket.endpoint, this.name); let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "down", "--remove-orphans" ], this.path); if (exitCode !== 0) { throw new Error("Failed to delete, please check the terminal output for more information."); @@ -388,7 +389,7 @@ export class Stack { } async start(socket: DockgeSocket) { - const terminalName = getComposeTerminalName(this.name); + const terminalName = getComposeTerminalName(socket.endpoint, this.name); let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "up", "-d", "--remove-orphans" ], this.path); if (exitCode !== 0) { throw new Error("Failed to start, please check the terminal output for more information."); @@ -397,7 +398,7 @@ export class Stack { } async stop(socket: DockgeSocket) : Promise { - const terminalName = getComposeTerminalName(this.name); + const terminalName = getComposeTerminalName(socket.endpoint, this.name); let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "stop" ], this.path); if (exitCode !== 0) { throw new Error("Failed to stop, please check the terminal output for more information."); @@ -406,7 +407,7 @@ export class Stack { } async restart(socket: DockgeSocket) : Promise { - const terminalName = getComposeTerminalName(this.name); + const terminalName = getComposeTerminalName(socket.endpoint, this.name); let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "restart" ], this.path); if (exitCode !== 0) { throw new Error("Failed to restart, please check the terminal output for more information."); @@ -415,7 +416,7 @@ export class Stack { } async down(socket: DockgeSocket) : Promise { - const terminalName = getComposeTerminalName(this.name); + const terminalName = getComposeTerminalName(socket.endpoint, this.name); let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "down" ], this.path); if (exitCode !== 0) { throw new Error("Failed to down, please check the terminal output for more information."); @@ -424,7 +425,7 @@ export class Stack { } async update(socket: DockgeSocket) { - const terminalName = getComposeTerminalName(this.name); + const terminalName = getComposeTerminalName(socket.endpoint, this.name); let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "pull" ], this.path); if (exitCode !== 0) { throw new Error("Failed to pull, please check the terminal output for more information."); @@ -445,7 +446,7 @@ export class Stack { } async joinCombinedTerminal(socket: DockgeSocket) { - const terminalName = getCombinedTerminalName(this.name); + const terminalName = getCombinedTerminalName(socket.endpoint, this.name); const terminal = Terminal.getOrCreateTerminal(this.server, terminalName, "docker", [ "compose", "logs", "-f", "--tail", "100" ], this.path); terminal.enableKeepAlive = true; terminal.rows = COMBINED_TERMINAL_ROWS; @@ -455,7 +456,7 @@ export class Stack { } async leaveCombinedTerminal(socket: DockgeSocket) { - const terminalName = getCombinedTerminalName(this.name); + const terminalName = getCombinedTerminalName(socket.endpoint, this.name); const terminal = Terminal.getTerminal(terminalName); if (terminal) { terminal.leave(socket); @@ -463,7 +464,7 @@ export class Stack { } async joinContainerTerminal(socket: DockgeSocket, serviceName: string, shell : string = "sh", index: number = 0) { - const terminalName = getContainerExecTerminalName(this.name, serviceName, index); + const terminalName = getContainerExecTerminalName(socket.endpoint, this.name, serviceName, index); let terminal = Terminal.getTerminal(terminalName); if (!terminal) { diff --git a/backend/terminal.ts b/backend/terminal.ts index dedc4e0..9eb6747 100644 --- a/backend/terminal.ts +++ b/backend/terminal.ts @@ -8,7 +8,7 @@ import { PROGRESS_TERMINAL_ROWS, TERMINAL_COLS, TERMINAL_ROWS -} from "./util-common"; +} from "../common/util-common"; import { sync as commandExistsSync } from "command-exists"; import { log } from "./log"; diff --git a/backend/util-server.ts b/backend/util-server.ts index 0277346..f4f53ee 100644 --- a/backend/util-server.ts +++ b/backend/util-server.ts @@ -2,10 +2,11 @@ import { Socket } from "socket.io"; import { Terminal } from "./terminal"; import { randomBytes } from "crypto"; import { log } from "./log"; -import { ERROR_TYPE_VALIDATION } from "./util-common"; +import { ERROR_TYPE_VALIDATION } from "../common/util-common"; import { R } from "redbean-node"; import { verifyPassword } from "./password-hash"; import fs from "fs"; +import { DockgeInstanceManager } from "./dockge-instance-manager"; export interface JWTDecoded { username : string; @@ -15,6 +16,9 @@ export interface JWTDecoded { export interface DockgeSocket extends Socket { userID: number; consoleTerminal? : Terminal; + instanceManager : DockgeInstanceManager; + endpoint : string; + emitAgent : (eventName : string, ...args : unknown[]) => void; } // For command line arguments, so they are nullable diff --git a/common/agent-socket.ts b/common/agent-socket.ts new file mode 100644 index 0000000..6748e44 --- /dev/null +++ b/common/agent-socket.ts @@ -0,0 +1,15 @@ +export class AgentSocket { + + eventList : Map void> = new Map(); + + on(event : string, callback : (...args : unknown[]) => void) { + this.eventList.set(event, callback); + } + + call(eventName : string, ...args : unknown[]) { + const callback = this.eventList.get(eventName); + if (callback) { + callback(...args); + } + } +} diff --git a/backend/util-common.ts b/common/util-common.ts similarity index 94% rename from backend/util-common.ts rename to common/util-common.ts index 433994c..5d4bb21 100644 --- a/backend/util-common.ts +++ b/common/util-common.ts @@ -206,20 +206,20 @@ export function getCryptoRandomInt(min: number, max: number):number { } } -export function getComposeTerminalName(stack : string) { - return "compose-" + stack; +export function getComposeTerminalName(endpoint : string, stack : string) { + return "compose-" + endpoint + "-" + stack; } -export function getCombinedTerminalName(stack : string) { - return "combined-" + stack; +export function getCombinedTerminalName(endpoint : string, stack : string) { + return "combined-" + endpoint + "-" + stack; } -export function getContainerTerminalName(container : string) { - return "container-" + container; +export function getContainerTerminalName(endpoint : string, container : string) { + return "container-" + endpoint + "-" + container; } -export function getContainerExecTerminalName(stackName : string, container : string, index : number) { - return "container-exec-" + stackName + "-" + container + "-" + index; +export function getContainerExecTerminalName(endpoint : string, stackName : string, container : string, index : number) { + return "container-exec-" + endpoint + "-" + stackName + "-" + container + "-" + index; } export function copyYAMLComments(doc : Document, src : Document) { diff --git a/extra/reset-password.ts b/extra/reset-password.ts index 4ba51ee..f6de5e5 100644 --- a/extra/reset-password.ts +++ b/extra/reset-password.ts @@ -5,7 +5,7 @@ import { User } from "../backend/models/user"; import { DockgeServer } from "../backend/dockge-server"; import { log } from "../backend/log"; import { io } from "socket.io-client"; -import { BaseRes } from "../backend/util-common"; +import { BaseRes } from "../common/util-common"; console.log("== Dockge Reset Password Tool =="); diff --git a/frontend/src/components/Container.vue b/frontend/src/components/Container.vue index 0a20182..b18c06c 100644 --- a/frontend/src/components/Container.vue +++ b/frontend/src/components/Container.vue @@ -137,7 +137,7 @@