Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d76b2f2
Global .env page and getting ports from running container
syko9000 Jan 15, 2024
6e7a383
Update i18n.ts with catalan language (#377)
pacoculebras Jan 17, 2024
bb67969
Added translation using Weblate (Catalan)
pacoculebras Jan 17, 2024
19b060b
Translated using Weblate (Catalan)
pacoculebras Jan 17, 2024
58d345c
Add translate key (#368)
cyril59310 Jan 17, 2024
504f2d6
Workaround fix for tsx issue (#380)
louislam Jan 18, 2024
833d0cb
Merge branch 'master' into master.global.env
syko9000 Jan 21, 2024
8e362f3
devcontainer config
Felioh Feb 24, 2024
26f8602
add git-managed stacks (pull only)
Felioh Mar 30, 2024
b946b11
add exit code to error message
Felioh Apr 29, 2024
26bd199
added support for multiple stacks in repos
Felioh Nov 16, 2024
fc4ad7f
draft support for nested stacks directory
mkoo21 Dec 13, 2024
2c29aea
Add discovery search for projects within stacks directory that are not
mkoo21 Dec 14, 2024
407654f
Merge remote-tracking branch 'origin-mkoo21/master' into support-nest…
tobi1449 Feb 25, 2025
d4245f2
Update readme with new merged PR
tobi1449 Feb 25, 2025
503a7ff
Merge remote-tracking branch 'origin-Felioh/multiple-stacks' into git…
tobi1449 Feb 25, 2025
cf1f10c
Merge remote-tracking branch 'origin-syko9000/master.global.env' into…
tobi1449 Feb 25, 2025
e124bd0
Return an empty stack list for getServiceStatusList if the stack does…
tobi1449 Feb 25, 2025
ade068c
Don't call requestServiceStatus if the initially shown page on load i…
tobi1449 Feb 25, 2025
e637410
Don't show "Save Stack" button if the stack is a gitops stack that is…
tobi1449 Feb 25, 2025
85cfeaf
Only fill json properties for git repos if the stack is actually base…
tobi1449 Feb 25, 2025
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ View Video: https://youtu.be/AWAlOQeNpgU?t=48
- 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;
}
}
18 changes: 18 additions & 0 deletions backend/socket-handlers/main-socket-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
import { passwordStrength } from "check-password-strength";
import jwt from "jsonwebtoken";
import { Settings } from "../settings";
import fs, { promises as fsAsync } from "fs";
import path from "path";

export class MainSocketHandler extends SocketHandler {
create(socket : DockgeSocket, server : DockgeServer) {
Expand Down Expand Up @@ -242,6 +244,12 @@ export class MainSocketHandler extends SocketHandler {
checkLogin(socket);
const data = await Settings.getSettings("general");

if (fs.existsSync(path.join(server.stacksDir, "global.env"))) {
data.globalENV = fs.readFileSync(path.join(server.stacksDir, "global.env"), "utf-8");
} else {
data.globalENV = "# VARIABLE=value #comment";
}

callback({
ok: true,
data: data,
Expand Down Expand Up @@ -270,6 +278,16 @@ export class MainSocketHandler extends SocketHandler {
if (!currentDisabledAuth && data.disableAuth) {
await doubleCheckPassword(socket, currentPassword);
}
// Handle global.env
if (data.globalENV && data.globalENV != "# VARIABLE=value #comment") {
await fsAsync.writeFile(path.join(server.stacksDir, "global.env"), data.globalENV);
} else {
await fsAsync.rm(path.join(server.stacksDir, "global.env"), {
recursive: true,
force: true
});
}
delete data.globalENV;

await Settings.setSettings("general", data);

Expand Down
Loading