mirror of
https://github.com/louislam/dockge.git
synced 2025-08-19 13:38:06 +02:00
wip
This commit is contained in:
@@ -26,6 +26,8 @@ import expressStaticGzip from "express-static-gzip";
|
||||
import path from "path";
|
||||
import { TerminalSocketHandler } from "./socket-handlers/terminal-socket-handler";
|
||||
import { Stack } from "./stack";
|
||||
import { Cron } from "croner";
|
||||
import childProcess from "child_process";
|
||||
|
||||
export class DockgeServer {
|
||||
app : Express;
|
||||
@@ -248,6 +250,15 @@ export class DockgeServer {
|
||||
} else {
|
||||
log.info("server", `Listening on ${this.config.port}`);
|
||||
}
|
||||
|
||||
// Run every 5 seconds
|
||||
const job = Cron("*/2 * * * * *", {
|
||||
protect: true, // Enabled over-run protection.
|
||||
}, () => {
|
||||
log.debug("server", "Cron job running");
|
||||
this.sendStackList(true);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@@ -412,18 +423,40 @@ export class DockgeServer {
|
||||
return jwtSecretBean;
|
||||
}
|
||||
|
||||
sendStackList(socket : DockgeSocket) {
|
||||
let room = socket.userID.toString();
|
||||
let stackList = Stack.getStackList(this);
|
||||
let list = {};
|
||||
|
||||
for (let stack of stackList) {
|
||||
list[stack.name] = stack.toSimpleJSON();
|
||||
sendStackList(useCache = false) {
|
||||
let stackList = Stack.getStackList(this, useCache);
|
||||
let roomList = this.io.sockets.adapter.rooms.keys();
|
||||
for (let room of roomList) {
|
||||
// Check if the room is a number (user id)
|
||||
if (Number(room)) {
|
||||
this.io.to(room).emit("stackList", {
|
||||
ok: true,
|
||||
stackList: Object.fromEntries(stackList),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.io.to(room).emit("stackList", {
|
||||
ok: true,
|
||||
stackList: list,
|
||||
});
|
||||
sendStackStatusList() {
|
||||
let statusList = Stack.getStatusList();
|
||||
|
||||
let roomList = this.io.sockets.adapter.rooms.keys();
|
||||
|
||||
for (let room of roomList) {
|
||||
// Check if the room is a number (user id)
|
||||
if (Number(room)) {
|
||||
log.debug("server", "Send stack status list to room " + room);
|
||||
this.io.to(room).emit("stackStatusList", {
|
||||
ok: true,
|
||||
stackStatusList: Object.fromEntries(statusList),
|
||||
});
|
||||
} else {
|
||||
log.debug("server", "Skip sending stack status list to room " + room);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get stackDirFullPath() {
|
||||
return path.resolve(this.stacksDir);
|
||||
}
|
||||
}
|
||||
|
@@ -1,18 +1,6 @@
|
||||
import { SocketHandler } from "../socket-handler.js";
|
||||
import { DockgeServer } from "../dockge-server";
|
||||
import { callbackError, checkLogin, DockgeSocket, ValidationError } from "../util-server";
|
||||
import { log } from "../log";
|
||||
import yaml from "yaml";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import {
|
||||
allowedCommandList,
|
||||
allowedRawKeys,
|
||||
getComposeTerminalName,
|
||||
isDev,
|
||||
PROGRESS_TERMINAL_ROWS
|
||||
} from "../util-common";
|
||||
import { Terminal } from "../terminal";
|
||||
import { Stack } from "../stack";
|
||||
|
||||
export class DockerSocketHandler extends SocketHandler {
|
||||
@@ -23,6 +11,7 @@ export class DockerSocketHandler extends SocketHandler {
|
||||
checkLogin(socket);
|
||||
const stack = this.saveStack(socket, server, name, composeYAML, isAdd);
|
||||
await stack.deploy(socket);
|
||||
server.sendStackList();
|
||||
callback({
|
||||
ok: true,
|
||||
});
|
||||
@@ -39,7 +28,33 @@ export class DockerSocketHandler extends SocketHandler {
|
||||
ok: true,
|
||||
"msg": "Saved"
|
||||
});
|
||||
server.sendStackList(socket);
|
||||
server.sendStackList();
|
||||
} catch (e) {
|
||||
callbackError(e, callback);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("deleteStack", async (name : unknown, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
if (typeof(name) !== "string") {
|
||||
throw new ValidationError("Name must be a string");
|
||||
}
|
||||
const stack = Stack.getStack(server, name);
|
||||
|
||||
try {
|
||||
await stack.delete(socket);
|
||||
} catch (e) {
|
||||
server.sendStackList();
|
||||
throw e;
|
||||
}
|
||||
|
||||
server.sendStackList();
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Deleted"
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callbackError(e, callback);
|
||||
}
|
||||
|
@@ -5,8 +5,14 @@ import { log } from "../log";
|
||||
import yaml from "yaml";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import { allowedCommandList, allowedRawKeys, isDev } from "../util-common";
|
||||
import { Terminal } from "../terminal";
|
||||
import {
|
||||
allowedCommandList,
|
||||
allowedRawKeys,
|
||||
getComposeTerminalName,
|
||||
isDev,
|
||||
PROGRESS_TERMINAL_ROWS
|
||||
} from "../util-common";
|
||||
import { MainTerminal, Terminal } from "../terminal";
|
||||
|
||||
export class TerminalSocketHandler extends SocketHandler {
|
||||
create(socket : DockgeSocket, server : DockgeServer) {
|
||||
@@ -57,12 +63,36 @@ export class TerminalSocketHandler extends SocketHandler {
|
||||
});
|
||||
|
||||
// Create Terminal
|
||||
socket.on("terminalCreate", async (terminalName : unknown, callback : unknown) => {
|
||||
socket.on("mainTerminal", async (terminalName : unknown, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
if (typeof(terminalName) !== "string") {
|
||||
throw new ValidationError("Terminal name must be a string.");
|
||||
}
|
||||
|
||||
log.debug("deployStack", "Terminal name: " + terminalName);
|
||||
|
||||
let terminal = Terminal.getTerminal(terminalName);
|
||||
|
||||
if (!terminal) {
|
||||
terminal = new MainTerminal(server, terminalName);
|
||||
terminal.rows = 50;
|
||||
log.debug("deployStack", "Terminal created");
|
||||
}
|
||||
|
||||
terminal.join(socket);
|
||||
terminal.start();
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
});
|
||||
} catch (e) {
|
||||
callbackError(e, callback);
|
||||
}
|
||||
});
|
||||
|
||||
// Join Terminal
|
||||
socket.on("terminalJoin", async (terminalName : unknown, callback : unknown) => {
|
||||
socket.on("terminalJoin", async (terminalName : unknown, callback) => {
|
||||
if (typeof(callback) !== "function") {
|
||||
log.debug("console", "Callback is not a function.");
|
||||
return;
|
||||
|
169
backend/stack.ts
169
backend/stack.ts
@@ -4,15 +4,28 @@ import { log } from "./log";
|
||||
import yaml from "yaml";
|
||||
import { DockgeSocket, ValidationError } from "./util-server";
|
||||
import path from "path";
|
||||
import { getComposeTerminalName, PROGRESS_TERMINAL_ROWS } from "./util-common";
|
||||
import {
|
||||
CREATED_FILE,
|
||||
CREATED_STACK,
|
||||
EXITED,
|
||||
getComposeTerminalName,
|
||||
PROGRESS_TERMINAL_ROWS,
|
||||
RUNNING,
|
||||
UNKNOWN
|
||||
} from "./util-common";
|
||||
import { Terminal } from "./terminal";
|
||||
import childProcess from "child_process";
|
||||
|
||||
export class Stack {
|
||||
|
||||
name: string;
|
||||
protected _status: number = UNKNOWN;
|
||||
protected _composeYAML?: string;
|
||||
protected _configFilePath?: string;
|
||||
protected server: DockgeServer;
|
||||
|
||||
protected static managedStackList: Map<string, Stack> = new Map();
|
||||
|
||||
constructor(server : DockgeServer, name : string, composeYAML? : string) {
|
||||
this.name = name;
|
||||
this.server = server;
|
||||
@@ -24,16 +37,30 @@ export class Stack {
|
||||
return {
|
||||
...obj,
|
||||
composeYAML: this.composeYAML,
|
||||
isManagedByDockge: this.isManagedByDockge,
|
||||
};
|
||||
}
|
||||
|
||||
toSimpleJSON() : object {
|
||||
return {
|
||||
name: this.name,
|
||||
status: this._status,
|
||||
tags: [],
|
||||
};
|
||||
}
|
||||
|
||||
get isManagedByDockge() : boolean {
|
||||
if (this._configFilePath) {
|
||||
return this._configFilePath.startsWith(this.server.stackDirFullPath) && fs.existsSync(this.path) && fs.statSync(this.path).isDirectory();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
get status() : number {
|
||||
return this._status;
|
||||
}
|
||||
|
||||
validate() {
|
||||
// Check name, allows [a-z][A-Z][0-9] _ - only
|
||||
if (!this.name.match(/^[a-zA-Z0-9_-]+$/)) {
|
||||
@@ -101,7 +128,7 @@ export class Stack {
|
||||
fs.writeFileSync(path.join(dir, "compose.yaml"), this.composeYAML);
|
||||
}
|
||||
|
||||
async deploy(socket? : DockgeSocket) : Promise<number> {
|
||||
deploy(socket? : DockgeSocket) : Promise<number> {
|
||||
const terminalName = getComposeTerminalName(this.name);
|
||||
log.debug("deployStack", "Terminal name: " + terminalName);
|
||||
|
||||
@@ -129,30 +156,138 @@ export class Stack {
|
||||
});
|
||||
}
|
||||
|
||||
static getStackList(server : DockgeServer) : Stack[] {
|
||||
let stacksDir = server.stacksDir;
|
||||
let stackList : Stack[] = [];
|
||||
delete(socket?: DockgeSocket) : Promise<number> {
|
||||
// Docker compose down
|
||||
const terminalName = getComposeTerminalName(this.name);
|
||||
log.debug("deleteStack", "Terminal name: " + terminalName);
|
||||
|
||||
// Scan the stacks directory, and get the stack list
|
||||
let filenameList = fs.readdirSync(stacksDir);
|
||||
const terminal = new Terminal(this.server, terminalName, "docker-compose", [ "down" ], this.path);
|
||||
|
||||
log.debug("stack", filenameList);
|
||||
terminal.rows = PROGRESS_TERMINAL_ROWS;
|
||||
|
||||
for (let filename of filenameList) {
|
||||
let relativePath = path.join(stacksDir, filename);
|
||||
if (fs.statSync(relativePath).isDirectory()) {
|
||||
let stack = new Stack(server, filename);
|
||||
stackList.push(stack);
|
||||
}
|
||||
if (socket) {
|
||||
terminal.join(socket);
|
||||
log.debug("deployStack", "Terminal joined");
|
||||
} else {
|
||||
log.debug("deployStack", "No socket, not joining");
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
terminal.onExit((exitCode : number) => {
|
||||
if (exitCode === 0) {
|
||||
// Remove the stack folder
|
||||
try {
|
||||
fs.rmSync(this.path, {
|
||||
recursive: true,
|
||||
force: true
|
||||
});
|
||||
resolve(exitCode);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
} else {
|
||||
reject(new Error("Failed to delete, please check the terminal output for more information."));
|
||||
}
|
||||
});
|
||||
terminal.start();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
static getStackList(server : DockgeServer, useCacheForManaged = false) : Map<string, Stack> {
|
||||
let stacksDir = server.stacksDir;
|
||||
let stackList : Map<string, Stack>;
|
||||
|
||||
if (useCacheForManaged && this.managedStackList.size > 0) {
|
||||
stackList = this.managedStackList;
|
||||
} else {
|
||||
stackList = new Map<string, Stack>();
|
||||
|
||||
// Scan the stacks directory, and get the stack list
|
||||
let filenameList = fs.readdirSync(stacksDir);
|
||||
|
||||
for (let filename of filenameList) {
|
||||
try {
|
||||
let stack = this.getStack(server, filename);
|
||||
stack._status = CREATED_FILE;
|
||||
stackList.set(filename, stack);
|
||||
} catch (e) {
|
||||
log.warn("getStackList", `Failed to get stack ${filename}, error: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Cache by copying
|
||||
this.managedStackList = new Map(stackList);
|
||||
}
|
||||
|
||||
// Also get the list from `docker compose ls --all --format json`
|
||||
let res = childProcess.execSync("docker compose ls --all --format json");
|
||||
let composeList = JSON.parse(res.toString());
|
||||
|
||||
for (let composeStack of composeList) {
|
||||
let stack = stackList.get(composeStack.Name);
|
||||
|
||||
// This stack probably is not managed by Dockge, but we still want to show it
|
||||
if (!stack) {
|
||||
stack = new Stack(server, composeStack.Name);
|
||||
stackList.set(composeStack.Name, stack);
|
||||
}
|
||||
|
||||
stack._status = this.statusConvert(composeStack.Status);
|
||||
stack._configFilePath = composeStack.ConfigFiles;
|
||||
}
|
||||
|
||||
return stackList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status list, it will be used to update the status of the stacks
|
||||
* Not all status will be returned, only the stack that is deployed or created to `docker compose` will be returned
|
||||
*/
|
||||
static getStatusList() : Map<string, number> {
|
||||
let statusList = new Map<string, number>();
|
||||
|
||||
let res = childProcess.execSync("docker compose ls --all --format json");
|
||||
let composeList = JSON.parse(res.toString());
|
||||
|
||||
for (let composeStack of composeList) {
|
||||
statusList.set(composeStack.Name, this.statusConvert(composeStack.Status));
|
||||
}
|
||||
|
||||
return statusList;
|
||||
}
|
||||
|
||||
static statusConvert(status : string) : number {
|
||||
if (status.startsWith("created")) {
|
||||
return CREATED_STACK;
|
||||
} else if (status.startsWith("running")) {
|
||||
return RUNNING;
|
||||
} else if (status.startsWith("exited")) {
|
||||
return EXITED;
|
||||
} else {
|
||||
return UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
static getStack(server: DockgeServer, stackName: string) : Stack {
|
||||
let dir = path.join(server.stacksDir, stackName);
|
||||
if (!fs.existsSync(dir)) {
|
||||
throw new ValidationError("Stack not found");
|
||||
|
||||
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
||||
// Maybe it is a stack managed by docker compose directly
|
||||
let stackList = this.getStackList(server);
|
||||
let stack = stackList.get(stackName);
|
||||
|
||||
if (stack) {
|
||||
return stack;
|
||||
} else {
|
||||
// Really not found
|
||||
throw new ValidationError("Stack not found");
|
||||
}
|
||||
}
|
||||
return new Stack(server, stackName);
|
||||
|
||||
let stack = new Stack(server, stackName);
|
||||
stack._status = UNKNOWN;
|
||||
stack._configFilePath = path.resolve(dir);
|
||||
return stack;
|
||||
}
|
||||
}
|
||||
|
@@ -47,6 +47,10 @@ export class Terminal {
|
||||
}
|
||||
|
||||
public start() {
|
||||
if (this._ptyProcess) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._ptyProcess = pty.spawn(this.file, this.args, {
|
||||
name: this.name,
|
||||
cwd: this.cwd,
|
||||
@@ -139,10 +143,12 @@ export class MainTerminal extends InteractiveTerminal {
|
||||
constructor(server : DockgeServer, name : string, cwd : string = "./") {
|
||||
let shell;
|
||||
|
||||
if (commandExistsSync("pwsh")) {
|
||||
shell = "pwsh";
|
||||
} else if (os.platform() === "win32") {
|
||||
shell = "powershell.exe";
|
||||
if (os.platform() === "win32") {
|
||||
if (commandExistsSync("pwsh.exe")) {
|
||||
shell = "pwsh.exe";
|
||||
} else {
|
||||
shell = "powershell.exe";
|
||||
}
|
||||
} else {
|
||||
shell = "bash";
|
||||
}
|
||||
|
@@ -1,7 +1,15 @@
|
||||
// For loading dayjs plugins, don't remove event though it is not used in this file
|
||||
/*
|
||||
* Common utilities for backend and frontend
|
||||
*/
|
||||
|
||||
// Init dayjs
|
||||
import dayjs from "dayjs";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
let randomBytes : (numBytes: number) => Uint8Array;
|
||||
|
||||
@@ -17,6 +25,58 @@ if (typeof window !== "undefined" && window.crypto) {
|
||||
randomBytes = (await import("node:crypto")).randomBytes;
|
||||
}
|
||||
|
||||
// Stack Status
|
||||
export const UNKNOWN = 0;
|
||||
export const CREATED_FILE = 1;
|
||||
export const CREATED_STACK = 2;
|
||||
export const RUNNING = 3;
|
||||
export const EXITED = 4;
|
||||
|
||||
export function statusName(status : number) : string {
|
||||
switch (status) {
|
||||
case CREATED_FILE:
|
||||
return "draft";
|
||||
case CREATED_STACK:
|
||||
return "created_stack";
|
||||
case RUNNING:
|
||||
return "running";
|
||||
case EXITED:
|
||||
return "exited";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
export function statusNameShort(status : number) : string {
|
||||
switch (status) {
|
||||
case CREATED_FILE:
|
||||
return "draft";
|
||||
case CREATED_STACK:
|
||||
return "inactive";
|
||||
case RUNNING:
|
||||
return "active";
|
||||
case EXITED:
|
||||
return "inactive";
|
||||
default:
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
|
||||
export function statusColor(status : number) : string {
|
||||
switch (status) {
|
||||
case CREATED_FILE:
|
||||
return "dark";
|
||||
case CREATED_STACK:
|
||||
return "danger";
|
||||
case RUNNING:
|
||||
return "primary";
|
||||
case EXITED:
|
||||
return "danger";
|
||||
default:
|
||||
return "secondary";
|
||||
}
|
||||
}
|
||||
|
||||
export const isDev = process.env.NODE_ENV === "development";
|
||||
export const TERMINAL_COLS = 80;
|
||||
export const TERMINAL_ROWS = 10;
|
||||
@@ -30,6 +90,7 @@ export const allowedCommandList : string[] = [
|
||||
"cd",
|
||||
"dir",
|
||||
];
|
||||
|
||||
export const allowedRawKeys = [
|
||||
"\u0003", // Ctrl + C
|
||||
];
|
||||
|
Reference in New Issue
Block a user