Skip to content

Commit de2de05

Browse files
authored
Multiple Dockge instances (louislam#200)
1 parent 80e885e commit de2de05

38 files changed

+1526
-598
lines changed

README.md

+8-11
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,17 @@ View Video: https://youtu.be/AWAlOQeNpgU?t=48
1414

1515
## ⭐ Features
1616

17-
- Manage `compose.yaml`
17+
- 🧑‍💼 Manage your `compose.yaml` files
1818
- Create/Edit/Start/Stop/Restart/Delete
1919
- Update Docker Images
20-
- Interactive Editor for `compose.yaml`
21-
- Interactive Web Terminal
22-
- Reactive
23-
- Everything is just responsive. Progress (Pull/Up/Down) and terminal output are in real-time
24-
- Easy-to-use & fancy UI
25-
- If you love Uptime Kuma's UI/UX, you will love this one too
26-
- Convert `docker run ...` commands into `compose.yaml`
27-
- File based structure
28-
- Dockge won't kidnap your compose files, they are stored on your drive as usual. You can interact with them using normal `docker compose` commands
20+
- ⌨️ Interactive Editor for `compose.yaml`
21+
- 🦦 Interactive Web Terminal
22+
- 🕷️ (1.4.0 🆕) Multiple agents support - You can manage multiple stacks from different Docker hosts in one single interface
23+
- 🏪 Convert `docker run ...` commands into `compose.yaml`
24+
- 📙 File based structure - Dockge won't kidnap your compose files, they are stored on your drive as usual. You can interact with them using normal `docker compose` commands
2925
<img src="https://github.com/louislam/dockge/assets/1336778/cc071864-592e-4909-b73a-343a57494002" width=300 />
30-
26+
- 🚄 Reactive - Everything is just responsive. Progress (Pull/Up/Down) and terminal output are in real-time
27+
- 🐣 Easy-to-use & fancy UI - If you love Uptime Kuma's UI/UX, you will love this one too
3128

3229
![](https://github.com/louislam/dockge/assets/1336778/89fc1023-b069-42c0-a01c-918c495f1a6a)
3330

backend/agent-manager.ts

+291
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
import { DockgeSocket } from "./util-server";
2+
import { io, Socket as SocketClient } from "socket.io-client";
3+
import { log } from "./log";
4+
import { Agent } from "./models/agent";
5+
import { isDev, LooseObject, sleep } from "../common/util-common";
6+
import semver from "semver";
7+
import { R } from "redbean-node";
8+
import dayjs, { Dayjs } from "dayjs";
9+
10+
/**
11+
* Dockge Instance Manager
12+
* One AgentManager per Socket connection
13+
*/
14+
export class AgentManager {
15+
16+
protected socket : DockgeSocket;
17+
protected agentSocketList : Record<string, SocketClient> = {};
18+
protected agentLoggedInList : Record<string, boolean> = {};
19+
protected _firstConnectTime : Dayjs = dayjs();
20+
21+
constructor(socket: DockgeSocket) {
22+
this.socket = socket;
23+
}
24+
25+
get firstConnectTime() : Dayjs {
26+
return this._firstConnectTime;
27+
}
28+
29+
test(url : string, username : string, password : string) : Promise<void> {
30+
return new Promise((resolve, reject) => {
31+
let obj = new URL(url);
32+
let endpoint = obj.host;
33+
34+
if (!endpoint) {
35+
reject(new Error("Invalid Dockge URL"));
36+
}
37+
38+
if (this.agentSocketList[endpoint]) {
39+
reject(new Error("The Dockge URL already exists"));
40+
}
41+
42+
let client = io(url, {
43+
reconnection: false,
44+
extraHeaders: {
45+
endpoint,
46+
}
47+
});
48+
49+
client.on("connect", () => {
50+
client.emit("login", {
51+
username: username,
52+
password: password,
53+
}, (res : LooseObject) => {
54+
if (res.ok) {
55+
resolve();
56+
} else {
57+
reject(new Error(res.msg));
58+
}
59+
client.disconnect();
60+
});
61+
});
62+
63+
client.on("connect_error", (err) => {
64+
if (err.message === "xhr poll error") {
65+
reject(new Error("Unable to connect to the Dockge instance"));
66+
} else {
67+
reject(err);
68+
}
69+
client.disconnect();
70+
});
71+
});
72+
}
73+
74+
/**
75+
*
76+
* @param url
77+
* @param username
78+
* @param password
79+
*/
80+
async add(url : string, username : string, password : string) : Promise<Agent> {
81+
let bean = R.dispense("agent") as Agent;
82+
bean.url = url;
83+
bean.username = username;
84+
bean.password = password;
85+
await R.store(bean);
86+
return bean;
87+
}
88+
89+
/**
90+
*
91+
* @param url
92+
*/
93+
async remove(url : string) {
94+
let bean = await R.findOne("agent", " url = ? ", [
95+
url,
96+
]);
97+
98+
if (bean) {
99+
await R.trash(bean);
100+
let endpoint = bean.endpoint;
101+
delete this.agentSocketList[endpoint];
102+
} else {
103+
throw new Error("Agent not found");
104+
}
105+
}
106+
107+
connect(url : string, username : string, password : string) {
108+
let obj = new URL(url);
109+
let endpoint = obj.host;
110+
111+
this.socket.emit("agentStatus", {
112+
endpoint: endpoint,
113+
status: "connecting",
114+
});
115+
116+
if (!endpoint) {
117+
log.error("agent-manager", "Invalid endpoint: " + endpoint + " URL: " + url);
118+
return;
119+
}
120+
121+
if (this.agentSocketList[endpoint]) {
122+
log.debug("agent-manager", "Already connected to the socket server: " + endpoint);
123+
return;
124+
}
125+
126+
log.info("agent-manager", "Connecting to the socket server: " + endpoint);
127+
let client = io(url, {
128+
extraHeaders: {
129+
endpoint,
130+
}
131+
});
132+
133+
client.on("connect", () => {
134+
log.info("agent-manager", "Connected to the socket server: " + endpoint);
135+
136+
client.emit("login", {
137+
username: username,
138+
password: password,
139+
}, (res : LooseObject) => {
140+
if (res.ok) {
141+
log.info("agent-manager", "Logged in to the socket server: " + endpoint);
142+
this.agentLoggedInList[endpoint] = true;
143+
this.socket.emit("agentStatus", {
144+
endpoint: endpoint,
145+
status: "online",
146+
});
147+
} else {
148+
log.error("agent-manager", "Failed to login to the socket server: " + endpoint);
149+
this.agentLoggedInList[endpoint] = false;
150+
this.socket.emit("agentStatus", {
151+
endpoint: endpoint,
152+
status: "offline",
153+
});
154+
}
155+
});
156+
});
157+
158+
client.on("connect_error", (err) => {
159+
log.error("agent-manager", "Error from the socket server: " + endpoint);
160+
this.socket.emit("agentStatus", {
161+
endpoint: endpoint,
162+
status: "offline",
163+
});
164+
});
165+
166+
client.on("disconnect", () => {
167+
log.info("agent-manager", "Disconnected from the socket server: " + endpoint);
168+
this.socket.emit("agentStatus", {
169+
endpoint: endpoint,
170+
status: "offline",
171+
});
172+
});
173+
174+
client.on("agent", (...args : unknown[]) => {
175+
this.socket.emit("agent", ...args);
176+
});
177+
178+
client.on("info", (res) => {
179+
log.debug("agent-manager", res);
180+
181+
// Disconnect if the version is lower than 1.4.0
182+
if (!isDev && semver.satisfies(res.version, "< 1.4.0")) {
183+
this.socket.emit("agentStatus", {
184+
endpoint: endpoint,
185+
status: "offline",
186+
msg: `${endpoint}: Unsupported version: ` + res.version,
187+
});
188+
client.disconnect();
189+
}
190+
});
191+
192+
this.agentSocketList[endpoint] = client;
193+
}
194+
195+
disconnect(endpoint : string) {
196+
let client = this.agentSocketList[endpoint];
197+
client?.disconnect();
198+
}
199+
200+
async connectAll() {
201+
this._firstConnectTime = dayjs();
202+
203+
if (this.socket.endpoint) {
204+
log.info("agent-manager", "This connection is connected as an agent, skip connectAll()");
205+
return;
206+
}
207+
208+
let list : Record<string, Agent> = await Agent.getAgentList();
209+
210+
if (Object.keys(list).length !== 0) {
211+
log.info("agent-manager", "Connecting to all instance socket server(s)...");
212+
}
213+
214+
for (let endpoint in list) {
215+
let agent = list[endpoint];
216+
this.connect(agent.url, agent.username, agent.password);
217+
}
218+
}
219+
220+
disconnectAll() {
221+
for (let endpoint in this.agentSocketList) {
222+
this.disconnect(endpoint);
223+
}
224+
}
225+
226+
async emitToEndpoint(endpoint: string, eventName: string, ...args : unknown[]) {
227+
log.debug("agent-manager", "Emitting event to endpoint: " + endpoint);
228+
let client = this.agentSocketList[endpoint];
229+
230+
if (!client) {
231+
log.error("agent-manager", "Socket client not found for endpoint: " + endpoint);
232+
throw new Error("Socket client not found for endpoint: " + endpoint);
233+
}
234+
235+
if (!client.connected || !this.agentLoggedInList[endpoint]) {
236+
// Maybe the request is too quick, the socket is not connected yet, check firstConnectTime
237+
// If it is within 10 seconds, we should apply retry logic here
238+
let diff = dayjs().diff(this.firstConnectTime, "second");
239+
log.debug("agent-manager", endpoint + ": diff: " + diff);
240+
let ok = false;
241+
while (diff < 10) {
242+
if (client.connected && this.agentLoggedInList[endpoint]) {
243+
log.debug("agent-manager", `${endpoint}: Connected & Logged in`);
244+
ok = true;
245+
break;
246+
}
247+
log.debug("agent-manager", endpoint + ": not ready yet, retrying in 1 second...");
248+
await sleep(1000);
249+
diff = dayjs().diff(this.firstConnectTime, "second");
250+
}
251+
252+
if (!ok) {
253+
log.error("agent-manager", `${endpoint}: Socket client not connected`);
254+
throw new Error("Socket client not connected for endpoint: " + endpoint);
255+
}
256+
}
257+
258+
client.emit("agent", endpoint, eventName, ...args);
259+
}
260+
261+
emitToAllEndpoints(eventName: string, ...args : unknown[]) {
262+
log.debug("agent-manager", "Emitting event to all endpoints");
263+
for (let endpoint in this.agentSocketList) {
264+
this.emitToEndpoint(endpoint, eventName, ...args).catch((e) => {
265+
log.warn("agent-manager", e.message);
266+
});
267+
}
268+
}
269+
270+
async sendAgentList() {
271+
let list = await Agent.getAgentList();
272+
let result : Record<string, LooseObject> = {};
273+
274+
// Myself
275+
result[""] = {
276+
url: "",
277+
username: "",
278+
endpoint: "",
279+
};
280+
281+
for (let endpoint in list) {
282+
let agent = list[endpoint];
283+
result[endpoint] = agent.toJSON();
284+
}
285+
286+
this.socket.emit("agentList", {
287+
ok: true,
288+
agentList: result,
289+
});
290+
}
291+
}

backend/agent-socket-handler.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { DockgeServer } from "./dockge-server";
2+
import { AgentSocket } from "../common/agent-socket";
3+
import { DockgeSocket } from "./util-server";
4+
5+
export abstract class AgentSocketHandler {
6+
abstract create(socket : DockgeSocket, server : DockgeServer, agentSocket : AgentSocket): void;
7+
}

0 commit comments

Comments
 (0)