This commit is contained in:
Louis Lam 2023-12-03 15:46:02 +08:00
parent 08813cbc29
commit 43a0c22e41
13 changed files with 204 additions and 109 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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