Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ data
stacks
tmp
/private
.pnpm-store

# Docker extra
docker
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ data
stacks
tmp
/private
.pnpm-store

# Git only
frontend-dist
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ View Video: https://youtu.be/AWAlOQeNpgU?t=48
- PR #637: Implement RIGHT and LEFT KEYS terminal navigation (by https://github.com/lukasondrejka)
- PR #642: Remove Useless Scrollbar (by https://github.com/cyril59310)
- PR #649: Add Container Control Buttons (by https://github.com/mizady)
- PR #670: GitOps with multiple stacks (by: https://github.com/Felioh)
- PR #685: Preserve YAML Comments (by https://github.com/turnah)
- PR #700: Add Resource Usage Stats (by https://github.com/justwiebe)
- PR #714: Conditional stack files deletion (by: https://github.com/husa)
- PR #687: Support for nested stacks directory (by: https://github.com/mkoo21)


## 🔧 How to Install
Expand Down
64 changes: 64 additions & 0 deletions backend/agent-socket-handlers/docker-socket-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { DockgeServer } from "../dockge-server";
import { callbackError, callbackResult, checkLogin, DockgeSocket, ValidationError } from "../util-server";
import { DeleteOptions, Stack } from "../stack";
import { AgentSocket } from "../../common/agent-socket";
import { Terminal } from "../terminal";
import { getComposeTerminalName } from "../../common/util-common";

export class DockerSocketHandler extends AgentSocketHandler {
create(socket : DockgeSocket, server : DockgeServer, agentSocket : AgentSocket) {
Expand All @@ -25,6 +27,47 @@ export class DockerSocketHandler extends AgentSocketHandler {
}
});

agentSocket.on("gitDeployStack", async (stackName : unknown, gitUrl : unknown, branch : unknown, isAdd : unknown, callback) => {
try {
checkLogin(socket);

if (typeof(stackName) !== "string") {
throw new ValidationError("Stack name must be a string");
}
if (typeof(gitUrl) !== "string") {
throw new ValidationError("Git URL must be a string");
}
if (typeof(branch) !== "string") {
throw new ValidationError("Git Ref must be a string");
}

const terminalName = getComposeTerminalName(socket.endpoint, stackName);

// TODO: this could be done smarter.
if (!isAdd) {
const stack = await Stack.getStack(server, stackName);
await stack.delete(socket);
}

let exitCode = await Terminal.exec(server, socket, terminalName, "git", [ "clone", "-b", branch, gitUrl, stackName ], server.stacksDir);
if (exitCode !== 0) {
throw new Error(`Failed to clone git repo [Exit Code ${exitCode}]`);
}

const stack = await Stack.getStack(server, stackName);
await stack.deploy(socket);

server.sendStackList();
callbackResult({
ok: true,
msg: "Deployed"
}, callback);
stack.joinCombinedTerminal(socket);
} catch (e) {
callbackError(e, callback);
}
});

agentSocket.on("saveStack", async (name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown, callback) => {
try {
checkLogin(socket);
Expand Down Expand Up @@ -197,6 +240,27 @@ export class DockerSocketHandler extends AgentSocketHandler {
}
});

// gitSync
agentSocket.on("gitSync", async (stackName : unknown, callback) => {
try {
checkLogin(socket);

if (typeof(stackName) !== "string") {
throw new ValidationError("Stack name must be a string");
}

const stack = await Stack.getStack(server, stackName);
await stack.gitSync(socket);
callbackResult({
ok: true,
msg: "Synced"
}, callback);
server.sendStackList();
} catch (e) {
callbackError(e, callback);
}
});

// down stack
agentSocket.on("downStack", async (stackName : unknown, callback) => {
try {
Expand Down
64 changes: 63 additions & 1 deletion backend/dockge-server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import "dotenv/config";
import { MainRouter } from "./routers/main-router";
import { WebhookRouter } from "./routers/webhook-router";
import * as fs from "node:fs";
import { PackageJson } from "type-fest";
import { Database } from "./database";
Expand All @@ -21,7 +22,7 @@ import { R } from "redbean-node";
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 { Arguments, Config, DockgeSocket, ValidationError } from "./util-server";
import { DockerSocketHandler } from "./agent-socket-handlers/docker-socket-handler";
import expressStaticGzip from "express-static-gzip";
import path from "path";
Expand All @@ -38,19 +39,23 @@ import { AgentSocket } from "../common/agent-socket";
import { ManageAgentSocketHandler } from "./socket-handlers/manage-agent-socket-handler";
import { Terminal } from "./terminal";

const GIT_UPDATE_CHECKER_INTERVAL_MS = 1000 * 60 * 10;

export class DockgeServer {
app : Express;
httpServer : http.Server;
packageJSON : PackageJson;
io : socketIO.Server;
config : Config;
indexHTML : string = "";
gitUpdateInterval : NodeJS.Timeout | undefined;

/**
* List of express routers
*/
routerList : Router[] = [
new MainRouter(),
new WebhookRouter(),
];

/**
Expand Down Expand Up @@ -204,6 +209,17 @@ export class DockgeServer {
};
}

// add a middleware to handle errors
this.app.use((err : unknown, _req : express.Request, res : express.Response, _next : express.NextFunction) => {
if (err instanceof Error) {
res.status(500).json({ error: err.message });
} else if (err instanceof ValidationError) {
res.status(400).json({ error: err.message });
} else {
res.status(500).json({ error: "Unknown error: " + err });
}
});

// Create Socket.io
this.io = new socketIO.Server(this.httpServer, {
cors,
Expand Down Expand Up @@ -398,6 +414,7 @@ export class DockgeServer {
});

checkVersion.startInterval();
this.startGitUpdater();
});

gracefulShutdown(this.httpServer, {
Expand Down Expand Up @@ -610,6 +627,47 @@ export class DockgeServer {
}
}

/**
* Start the git updater. This checks for outdated stacks and updates them.
* @param useCache
*/
async startGitUpdater(useCache = false) {
const check = async () => {
if (await Settings.get("gitAutoUpdate") !== true) {
return;
}

log.debug("git-updater", "checking for outdated stacks");

let socketList = this.io.sockets.sockets.values();

let stackList;
for (let socket of socketList) {
let dockgeSocket = socket as DockgeSocket;

// Get the list of stacks only once
if (!stackList) {
stackList = await Stack.getStackList(this, useCache);
}

for (let [ stackName, stack ] of stackList) {

if (stack.isGitRepo) {
stack.checkRemoteChanges().then(async (outdated) => {
if (outdated) {
log.info("git-updater", `Stack ${stackName} is outdated, Updating...`);
await stack.update(dockgeSocket);
}
});
}
}
}
};

await check();
this.gitUpdateInterval = setInterval(check, GIT_UPDATE_CHECKER_INTERVAL_MS) as NodeJS.Timeout;
}

async getDockerNetworkList() : Promise<string[]> {
let res = await childProcessAsync.spawn("docker", [ "network", "ls", "--format", "{{.Name}}" ], {
encoding: "utf-8",
Expand Down Expand Up @@ -673,6 +731,10 @@ export class DockgeServer {
log.info("server", "Shutdown requested");
log.info("server", "Called signal: " + signal);

if (this.gitUpdateInterval) {
clearInterval(this.gitUpdateInterval);
}

// TODO: Close all terminals?

await Database.close();
Expand Down
34 changes: 34 additions & 0 deletions backend/routers/webhook-router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { DockgeServer } from "../dockge-server";
import { log } from "../log";
import { Router } from "../router";
import express, { Express, Router as ExpressRouter } from "express";
import { Stack } from "../stack";

export class WebhookRouter extends Router {
create(app: Express, server: DockgeServer): ExpressRouter {
const router = express.Router();

router.get("/webhook/update/:stackname", async (req, res, _next) => {
try {
const stackname = req.params.stackname;

log.info("router", `Webhook received for stack: ${stackname}`);
const stack = await Stack.getStack(server, stackname);
if (!stack) {
log.error("router", `Stack not found: ${stackname}`);
res.status(404).json({ message: `Stack not found: ${stackname}` });
return;
}
await stack.gitSync(undefined);

// Send a response
res.json({ message: `Updated stack: ${stackname}` });

} catch (error) {
_next(error);
}
});

return router;
}
}
Loading