Louis Lam 1 year ago
parent
commit
43a0c22e41

+ 83 - 66
backend/dockge-instance-manager.ts

@@ -1,25 +1,89 @@
 import { DockgeSocket } from "./util-server";
-import { io } from "socket.io-client";
+import { io, Socket as SocketClient } from "socket.io-client";
 import { log } from "./log";
+import { addEndpointToTerminalName, convertToRemoteStackID } from "./util-common";
 
 /**
  * Dockge Instance Manager
  */
 export class DockgeInstanceManager {
-    protected static instance: DockgeInstanceManager;
 
-    protected constructor() {
+    protected socket : DockgeSocket;
+    protected instanceSocketList : Record<string, SocketClient> = {};
+
+    constructor(socket: DockgeSocket) {
+        this.socket = socket;
     }
 
-    public static getInstance(): DockgeInstanceManager {
-        if (!DockgeInstanceManager.instance) {
-            DockgeInstanceManager.instance = new DockgeInstanceManager();
+    connect(endpoint : string, tls : boolean, username : string, password : string) {
+        if (this.instanceSocketList[endpoint]) {
+            log.debug("INSTANCEMANAGER", "Already connected to the socket server: " + endpoint);
+            return;
         }
-        return DockgeInstanceManager.instance;
+
+        let url = ((tls) ? "wss://" : "ws://") + endpoint;
+
+        log.info("INSTANCEMANAGER", "Connecting to the socket server: " + endpoint);
+        let client = io(url, {
+            transports: [ "websocket", "polling" ],
+        });
+
+        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("stackList", (res) => {
+            if (res.endpoint) {
+                log.debug("INSTANCEMANAGER", "Received stackList from endpoint, ignore: " + res.endpoint);
+                return;
+            }
+
+            res.endpoint = endpoint;
+
+            let newStackList : Record<string, any> = {};
+
+            for (let stackName in res.stackList) {
+                let stack = res.stackList[stackName];
+                stack.endpoint = endpoint;
+                stack.id = convertToRemoteStackID(stack.name, endpoint);
+                newStackList[stack.name] = stack;
+            }
+            this.socket.emit("stackList", res);
+        });
+
+        client.on("terminalWrite", (terminalName, data) => {
+            this.socket.emit("terminalWrite", addEndpointToTerminalName(terminalName, endpoint), data);
+        });
+
+        this.instanceSocketList[endpoint] = client;
     }
 
-    connect(socket: DockgeSocket) {
+    disconnect(endpoint : string) {
+        let client = this.instanceSocketList[endpoint];
+        client?.disconnect();
+    }
 
+    connectAll() {
         let list : Record<string, {tls : boolean, username : string, password : string}> = {
             "louis-twister-pi:5001": {
                 tls: false,
@@ -34,67 +98,20 @@ export class DockgeInstanceManager {
 
         for (let endpoint in list) {
             let item = list[endpoint];
-
-            let url = (item.tls) ? "wss://" : "ws://";
-            url += endpoint;
-
-            log.info("INSTANCEMANAGER", "Connecting to the socket server: " + endpoint);
-            let client = io(url, {
-                transports: [ "websocket", "polling" ],
-            });
-
-            client.on("connect", () => {
-                log.info("INSTANCEMANAGER", "Connected to the socket server: " + endpoint);
-
-                client.emit("login", {
-                    username: item.username,
-                    password: item.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);
-            });
-
-            // Catch all events
-            client.onAny((eventName, ...args) => {
-                log.debug("INSTANCEMANAGER", "Received event: " + eventName);
-
-                let proxyEventList = [
-                    "stackList",
-                ];
-
-                if (proxyEventList.includes(eventName) &&
-                    args.length >= 1 &&
-                    typeof(args[0]) === "object" &&
-                    args[0].endpoint === undefined      // Only proxy the event from the endpoint, any upstream event will be ignored
-                ) {
-                    args[0].endpoint = endpoint;
-                    socket.emit(eventName, ...args);
-                } else {
-                    log.debug("INSTANCEMANAGER", "Event not in the proxy list or cannot set endpoint to the res: " + eventName);
-                }
-            });
-
-            socket.instanceSocketList[url] = client;
+            this.connect(endpoint, item.tls, item.username, item.password);
         }
     }
 
-    disconnect(socket: DockgeSocket) {
-        for (let url in socket.instanceSocketList) {
-            let client = socket.instanceSocketList[url];
-            client.disconnect();
+    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];
+        client?.emit(eventName, ...args);
+    }
+
 }

+ 8 - 12
backend/dockge-server.ts

@@ -68,8 +68,6 @@ export class DockgeServer {
 
     stacksDir : string = "";
 
-    dockgeInstanceManager : DockgeInstanceManager;
-
     /**
      *
      */
@@ -187,8 +185,6 @@ export class DockgeServer {
             response.send(this.indexHTML);
         });
 
-        this.dockgeInstanceManager = DockgeInstanceManager.getInstance();
-
         // Allow all CORS origins in development
         let cors = undefined;
         if (isDev) {
@@ -206,13 +202,14 @@ export class DockgeServer {
             log.info("server", "Socket connected!");
 
             let dockgeSocket = socket as DockgeSocket;
-            dockgeSocket.instanceSocketList = {};
+            dockgeSocket.isAgentMode = false;
+            dockgeSocket.instanceManager = new DockgeInstanceManager(dockgeSocket);
 
-            this.sendInfo(socket, true);
+            this.sendInfo(dockgeSocket, true);
 
             if (this.needSetup) {
                 log.info("server", "Redirect to setup page");
-                socket.emit("setup");
+                dockgeSocket.emit("setup");
             }
 
             // Create socket handlers
@@ -228,15 +225,15 @@ export class DockgeServer {
             if (await Settings.get("disableAuth")) {
                 log.info("auth", "Disabled Auth: auto login to admin");
                 this.afterLogin(dockgeSocket, await R.findOne("user") as User);
-                socket.emit("autoLogin");
+                dockgeSocket.emit("autoLogin");
             } else {
                 log.debug("auth", "need auth");
             }
 
             // Socket disconnect
-            socket.on("disconnect", () => {
+            dockgeSocket.on("disconnect", () => {
                 log.info("server", "Socket disconnected!");
-                this.dockgeInstanceManager.disconnect(dockgeSocket);
+                dockgeSocket.instanceManager.disconnectAll();
             });
 
         });
@@ -265,7 +262,7 @@ export class DockgeServer {
         }
 
         // Also connect to other dockge instances
-        this.dockgeInstanceManager.connect(socket);
+        socket.instanceManager.connectAll();
     }
 
     /**
@@ -525,7 +522,6 @@ export class DockgeServer {
                 this.io.to(room).emit("stackList", {
                     ok: true,
                     stackList: Object.fromEntries(map),
-                    endpoint: undefined,
                 });
             }
         }

+ 17 - 0
backend/socket-handler.ts

@@ -1,6 +1,23 @@
 import { DockgeServer } from "./dockge-server";
 import { DockgeSocket } from "./util-server";
+import { log } from "./log";
 
 export abstract class SocketHandler {
     abstract create(socket : DockgeSocket, server : DockgeServer): void;
+
+    event(eventName : string, socket : DockgeSocket, callback: (...args: any[]) => void) {
+
+        socket.on(eventName, (...args) => {
+            log.debug("SOCKET", "Received event: " + eventName);
+
+            let req = args[0];
+            let endpoint = req.endpoint;
+
+            if (endpoint) {
+                socket.instanceManager.emitToEndpoint(endpoint, eventName, ...args);
+            } else {
+                callback(...args);
+            }
+        });
+    }
 }

+ 5 - 3
backend/socket-handlers/docker-socket-handler.ts

@@ -5,6 +5,7 @@ import { Stack } from "../stack";
 
 // @ts-ignore
 import composerize from "composerize";
+import { convertToLocalStackName, convertToRemoteStackID, isRemoteStackName, LooseObject } from "../util-common";
 
 export class DockerSocketHandler extends SocketHandler {
     create(socket : DockgeSocket, server : DockgeServer) {
@@ -65,14 +66,15 @@ export class DockerSocketHandler extends SocketHandler {
             }
         });
 
-        socket.on("getStack", (stackName : unknown, callback) => {
+        this.event("getStack", socket, (req : LooseObject, callback) => {
             try {
                 checkLogin(socket);
 
-                if (typeof(stackName) !== "string") {
-                    throw new ValidationError("Stack name must be a string");
+                if (typeof(req) !== "object") {
+                    throw new ValidationError("Request must be an object");
                 }
 
+                let stackName = req.stackName;
                 const stack = Stack.getStack(server, stackName);
 
                 if (stack.isManagedByDockge) {

+ 0 - 2
backend/socket-handlers/main-socket-handler.ts

@@ -260,8 +260,6 @@ export class MainSocketHandler extends SocketHandler {
                     await doubleCheckPassword(socket, currentPassword);
                 }
 
-                console.log(data);
-
                 await Settings.setSettings("general", data);
 
                 callback({

+ 3 - 0
backend/stack.ts

@@ -21,6 +21,7 @@ import childProcess from "child_process";
 export class Stack {
 
     name: string;
+
     protected _status: number = UNKNOWN;
     protected _composeYAML?: string;
     protected _configFilePath?: string;
@@ -59,6 +60,8 @@ export class Stack {
     toSimpleJSON() : object {
         return {
             name: this.name,
+            id: this.name,
+            endpoint: undefined,
             status: this._status,
             tags: [],
             isManagedByDockge: this.isManagedByDockge,

+ 57 - 6
backend/util-common.ts

@@ -190,20 +190,34 @@ export function getCryptoRandomInt(min: number, max: number):number {
     }
 }
 
-export function getComposeTerminalName(stack : string) {
-    return "compose-" + stack;
+export function getComposeTerminalName(stackID : string) {
+    return "compose-" + stackID;
 }
 
-export function getCombinedTerminalName(stack : string) {
-    return "combined-" + stack;
+export function getCombinedTerminalName(stackID : string) {
+    return "combined-" + stackID;
 }
 
 export function getContainerTerminalName(container : string) {
     return "container-" + container;
 }
 
-export function getContainerExecTerminalName(stackName : string, container : string, index : number) {
-    return "container-exec-" + stackName + "-" + container + "-" + index;
+export function getContainerExecTerminalName(stackID : string, container : string, index : number) {
+    return "containerExec-" + stackID + "-" + container + "-" + index;
+}
+
+export function addEndpointToTerminalName(terminalName : string, endpoint : string) {
+    if (
+        terminalName.startsWith("compose-") ||
+        terminalName.startsWith("combined-") ||
+        terminalName.startsWith("containerExec-")
+    ) {
+        let arr = terminalName.split("-");
+        arr[1] = convertToRemoteStackID(arr[1], endpoint);
+        return arr.join("-");
+    } else {
+        return terminalName;
+    }
 }
 
 export function copyYAMLComments(doc : Document, src : Document) {
@@ -340,3 +354,40 @@ export function parseDockerPort(input : string, defaultHostname : string = "loca
         display: display,
     };
 }
+
+const splitChar : string = "::";
+
+export function convertToRemoteStackID(stackName? : string, endpoint? : string) {
+    if (!stackName || !endpoint) {
+        return stackName;
+    }
+
+    if (stackName.startsWith("remote" + splitChar)) {
+        return stackName;
+    }
+    return `remote${splitChar}${endpoint}${splitChar}${stackName}`;
+}
+
+export function convertToLocalStackName(stackName? : string) {
+    if (!stackName) {
+        return {
+            endpoint: undefined,
+            stackName: undefined,
+        };
+    }
+
+    if (!stackName.startsWith("remote" + splitChar)) {
+        return {
+            endpoint: undefined,
+            stackName,
+        };
+    }
+    return {
+        endpoint: stackName.split(splitChar)[1],
+        stackName: stackName.split(splitChar).splice(2).join(splitChar)
+    };
+}
+
+export function isRemoteStackName(stackName : string) {
+    return stackName.startsWith("remote" + splitChar);
+}

+ 3 - 1
backend/util-server.ts

@@ -5,6 +5,7 @@ import { log } from "./log";
 import { ERROR_TYPE_VALIDATION } from "./util-common";
 import { R } from "redbean-node";
 import { verifyPassword } from "./password-hash";
+import { DockgeInstanceManager } from "./dockge-instance-manager";
 
 export interface JWTDecoded {
     username : string;
@@ -14,7 +15,8 @@ export interface JWTDecoded {
 export interface DockgeSocket extends Socket {
     userID: number;
     consoleTerminal? : Terminal;
-    instanceSocketList: Record<string, SocketClient>;
+    instanceManager : DockgeInstanceManager;
+    isAgentMode : boolean;
 }
 
 // For command line arguments, so they are nullable

+ 9 - 0
frontend/src/components/StackList.vue

@@ -152,6 +152,15 @@ export default {
             });
 
             result.sort((m1, m2) => {
+
+                // sort by managed by dockge
+                if (m1.isManagedByDockge && !m2.isManagedByDockge) {
+                    return -1;
+                } else if (!m1.isManagedByDockge && m2.isManagedByDockge) {
+                    return 1;
+                }
+
+                // sort by status
                 if (m1.status !== m2.status) {
                     if (m2.status === RUNNING) {
                         return 1;

+ 3 - 5
frontend/src/components/StackListItem.vue

@@ -11,6 +11,7 @@
 <script>
 
 import Uptime from "./Uptime.vue";
+import { convertToLocalStackName } from "../../../backend/util-common";
 
 export default {
     components: {
@@ -55,10 +56,7 @@ export default {
     },
     computed: {
         url() {
-            if (!this.stack.endpoint) {
-                return `/compose/${this.stack.name}`;
-            }
-            return `/compose/${this.stack.name}/${this.stack.endpoint}`;
+            return `/compose/${this.stack.id}`;
         },
         depthMargin() {
             return {
@@ -66,7 +64,7 @@ export default {
             };
         },
         stackName() {
-            return this.stack.name;
+            return convertToLocalStackName(this.stack.name).stackName;
         }
     },
     watch: {

+ 0 - 1
frontend/src/mixins/socket.ts

@@ -199,7 +199,6 @@ export default defineComponent({
             });
 
             socket.on("stackList", (res) => {
-                console.log(res);
                 if (res.ok) {
                     if (!res.endpoint) {
                         this.stackList = res.stackList;

+ 15 - 8
frontend/src/pages/Compose.vue

@@ -214,7 +214,7 @@ import "vue-prism-editor/dist/prismeditor.min.css";
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
 import {
     COMBINED_TERMINAL_COLS,
-    COMBINED_TERMINAL_ROWS,
+    COMBINED_TERMINAL_ROWS, convertToLocalStackName,
     copyYAMLComments,
     getCombinedTerminalName,
     getComposeTerminalName,
@@ -322,17 +322,17 @@ export default {
         },
 
         terminalName() {
-            if (!this.stack.name) {
+            if (!this.stack.id) {
                 return "";
             }
-            return getComposeTerminalName(this.stack.name);
+            return getComposeTerminalName(this.stack.id);
         },
 
         combinedTerminalName() {
-            if (!this.stack.name) {
+            if (!this.stack.id) {
                 return "";
             }
-            return getCombinedTerminalName(this.stack.name);
+            return getCombinedTerminalName(this.stack.id);
         },
 
         networks() {
@@ -371,7 +371,7 @@ export default {
 
         $route(to, from) {
             // Leave Combined Terminal
-            console.debug("leaveCombinedTerminal", from.params.stackName);
+            console.debug("leaveCombinedTerminal", from.params.stackID);
             this.$root.getSocket().emit("leaveCombinedTerminal", this.stack.name, () => {});
         }
     },
@@ -400,7 +400,10 @@ export default {
             this.yamlCodeChange();
 
         } else {
-            this.stack.name = this.$route.params.stackName;
+            this.stack.id = this.$route.params.stackID;
+            let { endpoint, stackName } = convertToLocalStackName(this.stack.id);
+            this.stack.name = stackName;
+            this.stack.endpoint = endpoint;
             this.loadStack();
         }
 
@@ -448,7 +451,11 @@ export default {
 
         loadStack() {
             this.processing = true;
-            this.$root.getSocket().emit("getStack", this.stack.name, (res) => {
+
+            this.$root.getSocket().emit("getStack", {
+                stackName: this.stack.name,
+                endpoint: this.stack.endpoint,
+            }, (res) => {
                 if (res.ok) {
                     this.stack = res.stack;
                     this.yamlCodeChange();

+ 1 - 5
frontend/src/router.ts

@@ -35,14 +35,10 @@ const routes = [
                                 component: Compose,
                             },
                             {
-                                path: "/compose/:stackName",
+                                path: "/compose/:stackID",
                                 name: "compose",
                                 component: Compose,
                             },
-                            {
-                                path: "/compose/:stackName/:endpoint",
-                                component: Compose,
-                            },
                             {
                                 path: "/terminal/:stackName/:serviceName/:type",
                                 component: ContainerTerminal,