Skip to content

Commit ae560cc

Browse files
authored
Merge pull request #4 from tobi1449/gitops-multiple-stacks
Merge PR louislam#670: GitOps with multiple stacks
2 parents 45e5bc9 + d92dee5 commit ae560cc

File tree

17 files changed

+547
-64
lines changed

17 files changed

+547
-64
lines changed

.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ data
66
stacks
77
tmp
88
/private
9+
.pnpm-store
910

1011
# Docker extra
1112
docker

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ data
66
stacks
77
tmp
88
/private
9+
.pnpm-store
910

1011
# Git only
1112
frontend-dist

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,12 @@ View Video: https://youtu.be/AWAlOQeNpgU?t=48
4242
- PR #637: Implement RIGHT and LEFT KEYS terminal navigation (by https://github.com/lukasondrejka)
4343
- PR #642: Remove Useless Scrollbar (by https://github.com/cyril59310)
4444
- PR #649: Add Container Control Buttons (by https://github.com/mizady)
45+
- PR #670: GitOps with multiple stacks (by: https://github.com/Felioh)
4546
- PR #685: Preserve YAML Comments (by https://github.com/turnah)
4647
- PR #687: Support for nested stacks directory (by: https://github.com/mkoo21)
4748
- PR #700: Add Resource Usage Stats (by https://github.com/justwiebe)
4849
- PR #714: Conditional stack files deletion (by: https://github.com/husa)
50+
- PR #687: Support for nested stacks directory (by: https://github.com/mkoo21)
4951

5052

5153
## 🔧 How to Install

backend/agent-socket-handlers/docker-socket-handler.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { DockgeServer } from "../dockge-server";
33
import { callbackError, callbackResult, checkLogin, DockgeSocket, ValidationError } from "../util-server";
44
import { DeleteOptions, Stack } from "../stack";
55
import { AgentSocket } from "../../common/agent-socket";
6+
import { Terminal } from "../terminal";
7+
import { getComposeTerminalName } from "../../common/util-common";
68

79
export class DockerSocketHandler extends AgentSocketHandler {
810
create(socket : DockgeSocket, server : DockgeServer, agentSocket : AgentSocket) {
@@ -25,6 +27,47 @@ export class DockerSocketHandler extends AgentSocketHandler {
2527
}
2628
});
2729

30+
agentSocket.on("gitDeployStack", async (stackName : unknown, gitUrl : unknown, branch : unknown, isAdd : unknown, callback) => {
31+
try {
32+
checkLogin(socket);
33+
34+
if (typeof(stackName) !== "string") {
35+
throw new ValidationError("Stack name must be a string");
36+
}
37+
if (typeof(gitUrl) !== "string") {
38+
throw new ValidationError("Git URL must be a string");
39+
}
40+
if (typeof(branch) !== "string") {
41+
throw new ValidationError("Git Ref must be a string");
42+
}
43+
44+
const terminalName = getComposeTerminalName(socket.endpoint, stackName);
45+
46+
// TODO: this could be done smarter.
47+
if (!isAdd) {
48+
const stack = await Stack.getStack(server, stackName);
49+
await stack.delete(socket);
50+
}
51+
52+
let exitCode = await Terminal.exec(server, socket, terminalName, "git", [ "clone", "-b", branch, gitUrl, stackName ], server.stacksDir);
53+
if (exitCode !== 0) {
54+
throw new Error(`Failed to clone git repo [Exit Code ${exitCode}]`);
55+
}
56+
57+
const stack = await Stack.getStack(server, stackName);
58+
await stack.deploy(socket);
59+
60+
server.sendStackList();
61+
callbackResult({
62+
ok: true,
63+
msg: "Deployed"
64+
}, callback);
65+
stack.joinCombinedTerminal(socket);
66+
} catch (e) {
67+
callbackError(e, callback);
68+
}
69+
});
70+
2871
agentSocket.on("saveStack", async (name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown, callback) => {
2972
try {
3073
checkLogin(socket);
@@ -197,6 +240,27 @@ export class DockerSocketHandler extends AgentSocketHandler {
197240
}
198241
});
199242

243+
// gitSync
244+
agentSocket.on("gitSync", async (stackName : unknown, callback) => {
245+
try {
246+
checkLogin(socket);
247+
248+
if (typeof(stackName) !== "string") {
249+
throw new ValidationError("Stack name must be a string");
250+
}
251+
252+
const stack = await Stack.getStack(server, stackName);
253+
await stack.gitSync(socket);
254+
callbackResult({
255+
ok: true,
256+
msg: "Synced"
257+
}, callback);
258+
server.sendStackList();
259+
} catch (e) {
260+
callbackError(e, callback);
261+
}
262+
});
263+
200264
// down stack
201265
agentSocket.on("downStack", async (stackName : unknown, callback) => {
202266
try {

backend/dockge-server.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import "dotenv/config";
22
import { MainRouter } from "./routers/main-router";
3+
import { WebhookRouter } from "./routers/webhook-router";
34
import * as fs from "node:fs";
45
import { PackageJson } from "type-fest";
56
import { Database } from "./database";
@@ -21,7 +22,7 @@ import { R } from "redbean-node";
2122
import { genSecret, isDev, LooseObject } from "../common/util-common";
2223
import { generatePasswordHash } from "./password-hash";
2324
import { Bean } from "redbean-node/dist/bean";
24-
import { Arguments, Config, DockgeSocket } from "./util-server";
25+
import { Arguments, Config, DockgeSocket, ValidationError } from "./util-server";
2526
import { DockerSocketHandler } from "./agent-socket-handlers/docker-socket-handler";
2627
import expressStaticGzip from "express-static-gzip";
2728
import path from "path";
@@ -38,19 +39,23 @@ import { AgentSocket } from "../common/agent-socket";
3839
import { ManageAgentSocketHandler } from "./socket-handlers/manage-agent-socket-handler";
3940
import { Terminal } from "./terminal";
4041

42+
const GIT_UPDATE_CHECKER_INTERVAL_MS = 1000 * 60 * 10;
43+
4144
export class DockgeServer {
4245
app : Express;
4346
httpServer : http.Server;
4447
packageJSON : PackageJson;
4548
io : socketIO.Server;
4649
config : Config;
4750
indexHTML : string = "";
51+
gitUpdateInterval : NodeJS.Timeout | undefined;
4852

4953
/**
5054
* List of express routers
5155
*/
5256
routerList : Router[] = [
5357
new MainRouter(),
58+
new WebhookRouter(),
5459
];
5560

5661
/**
@@ -204,6 +209,17 @@ export class DockgeServer {
204209
};
205210
}
206211

212+
// add a middleware to handle errors
213+
this.app.use((err : unknown, _req : express.Request, res : express.Response, _next : express.NextFunction) => {
214+
if (err instanceof Error) {
215+
res.status(500).json({ error: err.message });
216+
} else if (err instanceof ValidationError) {
217+
res.status(400).json({ error: err.message });
218+
} else {
219+
res.status(500).json({ error: "Unknown error: " + err });
220+
}
221+
});
222+
207223
// Create Socket.io
208224
this.io = new socketIO.Server(this.httpServer, {
209225
cors,
@@ -398,6 +414,7 @@ export class DockgeServer {
398414
});
399415

400416
checkVersion.startInterval();
417+
this.startGitUpdater();
401418
});
402419

403420
gracefulShutdown(this.httpServer, {
@@ -610,6 +627,47 @@ export class DockgeServer {
610627
}
611628
}
612629

630+
/**
631+
* Start the git updater. This checks for outdated stacks and updates them.
632+
* @param useCache
633+
*/
634+
async startGitUpdater(useCache = false) {
635+
const check = async () => {
636+
if (await Settings.get("gitAutoUpdate") !== true) {
637+
return;
638+
}
639+
640+
log.debug("git-updater", "checking for outdated stacks");
641+
642+
let socketList = this.io.sockets.sockets.values();
643+
644+
let stackList;
645+
for (let socket of socketList) {
646+
let dockgeSocket = socket as DockgeSocket;
647+
648+
// Get the list of stacks only once
649+
if (!stackList) {
650+
stackList = await Stack.getStackList(this, useCache);
651+
}
652+
653+
for (let [ stackName, stack ] of stackList) {
654+
655+
if (stack.isGitRepo) {
656+
stack.checkRemoteChanges().then(async (outdated) => {
657+
if (outdated) {
658+
log.info("git-updater", `Stack ${stackName} is outdated, Updating...`);
659+
await stack.update(dockgeSocket);
660+
}
661+
});
662+
}
663+
}
664+
}
665+
};
666+
667+
await check();
668+
this.gitUpdateInterval = setInterval(check, GIT_UPDATE_CHECKER_INTERVAL_MS) as NodeJS.Timeout;
669+
}
670+
613671
async getDockerNetworkList() : Promise<string[]> {
614672
let res = await childProcessAsync.spawn("docker", [ "network", "ls", "--format", "{{.Name}}" ], {
615673
encoding: "utf-8",
@@ -673,6 +731,10 @@ export class DockgeServer {
673731
log.info("server", "Shutdown requested");
674732
log.info("server", "Called signal: " + signal);
675733

734+
if (this.gitUpdateInterval) {
735+
clearInterval(this.gitUpdateInterval);
736+
}
737+
676738
// TODO: Close all terminals?
677739

678740
await Database.close();

backend/routers/webhook-router.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { DockgeServer } from "../dockge-server";
2+
import { log } from "../log";
3+
import { Router } from "../router";
4+
import express, { Express, Router as ExpressRouter } from "express";
5+
import { Stack } from "../stack";
6+
7+
export class WebhookRouter extends Router {
8+
create(app: Express, server: DockgeServer): ExpressRouter {
9+
const router = express.Router();
10+
11+
router.get("/webhook/update/:stackname", async (req, res, _next) => {
12+
try {
13+
const stackname = req.params.stackname;
14+
15+
log.info("router", `Webhook received for stack: ${stackname}`);
16+
const stack = await Stack.getStack(server, stackname);
17+
if (!stack) {
18+
log.error("router", `Stack not found: ${stackname}`);
19+
res.status(404).json({ message: `Stack not found: ${stackname}` });
20+
return;
21+
}
22+
await stack.gitSync(undefined);
23+
24+
// Send a response
25+
res.json({ message: `Updated stack: ${stackname}` });
26+
27+
} catch (error) {
28+
_next(error);
29+
}
30+
});
31+
32+
return router;
33+
}
34+
}

0 commit comments

Comments
 (0)