diff --git a/backend/dockge-server.ts b/backend/dockge-server.ts index 8f734ccf..a629e042 100644 --- a/backend/dockge-server.ts +++ b/backend/dockge-server.ts @@ -45,6 +45,7 @@ export class DockgeServer { io : socketIO.Server; config : Config; indexHTML : string = ""; + dockerPath : string = ""; /** * List of express routers @@ -80,6 +81,7 @@ export class DockgeServer { stacksDir : string = ""; + /** * */ @@ -136,7 +138,11 @@ export class DockgeServer { stacksDir: { type: String, optional: true, - } + }, + dockerPath: { + type: String, + optional: true, + }, }); this.config = args as Config; @@ -149,7 +155,9 @@ export class DockgeServer { this.config.hostname = args.hostname || process.env.DOCKGE_HOSTNAME || undefined; this.config.dataDir = args.dataDir || process.env.DOCKGE_DATA_DIR || "./data/"; this.config.stacksDir = args.stacksDir || process.env.DOCKGE_STACKS_DIR || defaultStacksDir; + this.config.dockerPath = args.dockerPath || process.env.DOCKGE_DOCKER_PATH || "docker"; this.stacksDir = this.config.stacksDir; + this.dockerPath = this.config.dockerPath; log.debug("server", this.config); @@ -611,7 +619,7 @@ export class DockgeServer { } async getDockerNetworkList() : Promise { - let res = await childProcessAsync.spawn("docker", [ "network", "ls", "--format", "{{.Name}}" ], { + let res = await childProcessAsync.spawn(this.dockerPath, [ "network", "ls", "--format", "{{.Name}}" ], { encoding: "utf-8", }); diff --git a/backend/stack.ts b/backend/stack.ts index fbce5002..fc6a79d2 100644 --- a/backend/stack.ts +++ b/backend/stack.ts @@ -20,6 +20,12 @@ import { InteractiveTerminal, Terminal } from "./terminal"; import childProcessAsync from "promisify-child-process"; import { Settings } from "./settings"; +interface composeListStatus { + Name: string; + Status: string; + ConfigFiles: string; +} + export class Stack { name: string; @@ -93,7 +99,7 @@ export class Stack { * Get the status of the stack from `docker compose ps --format json` */ async ps() : Promise { - let res = await childProcessAsync.spawn("docker", [ "compose", "ps", "--format", "json" ], { + let res = await childProcessAsync.spawn(this.server.dockerPath, [ "compose", "ps", "--format", "json" ], { cwd: this.path, encoding: "utf-8", }); @@ -208,7 +214,7 @@ export class Stack { async deploy(socket : DockgeSocket) : Promise { const terminalName = getComposeTerminalName(socket.endpoint, this.name); - let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "up", "-d", "--remove-orphans" ], this.path); + let exitCode = await Terminal.exec(this.server, socket, terminalName, this.server.dockerPath, [ "compose", "up", "-d", "--remove-orphans" ], this.path); if (exitCode !== 0) { throw new Error("Failed to deploy, please check the terminal output for more information."); } @@ -217,7 +223,7 @@ export class Stack { async delete(socket: DockgeSocket) : Promise { const terminalName = getComposeTerminalName(socket.endpoint, this.name); - let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "down", "--remove-orphans" ], this.path); + let exitCode = await Terminal.exec(this.server, socket, terminalName, this.server.dockerPath, [ "compose", "down", "--remove-orphans" ], this.path); if (exitCode !== 0) { throw new Error("Failed to delete, please check the terminal output for more information."); } @@ -231,8 +237,8 @@ export class Stack { return exitCode; } - async updateStatus() { - let statusList = await Stack.getStatusList(); + async updateStatus(server: DockgeServer) { + let statusList = await Stack.getStatusList(server); let status = statusList.get(this.name); if (status) { @@ -301,15 +307,7 @@ export class Stack { } // Get status from docker compose ls - let res = await childProcessAsync.spawn("docker", [ "compose", "ls", "--all", "--format", "json" ], { - encoding: "utf-8", - }); - - if (!res.stdout) { - return stackList; - } - - let composeList = JSON.parse(res.stdout.toString()); + let composeList = await this.getStackComposeList(server);; for (let composeStack of composeList) { let stack = stackList.get(composeStack.Name); @@ -331,22 +329,85 @@ export class Stack { return stackList; } + static async getStackComposeList(server: DockgeServer): Promise> { + let stacksDir = server.stacksDir; + let statusList = new Array<{ Name: string, Status: string, ConfigFiles: string }>(); + + // Scan the stacks directory, and get the stack list + let filenameList = await fsAsync.readdir(stacksDir); + + for (const filename of filenameList) { + try { + // Check if it is a directory + let stat = await fsAsync.stat(path.join(stacksDir, filename)); + if (!stat.isDirectory()) { + continue; + } + + // Find existing compose files + let composeFile = ""; + let filenamePath = path.join(stacksDir, filename); + for (const filename of acceptedComposeFileNames) { + composeFile = path.join(filenamePath, filename); + if (await fileExists(composeFile)) { + break; + } + composeFile = "" + } + + // If no compose file exists, skip it + if (composeFile == "") { + continue + } + + let res = await childProcessAsync.spawn(server.dockerPath, ["compose", "-f", composeFile, "ps", "--all", "--format", "json"], { + encoding: "utf-8", + }); + + if (!res.stdout) { + return statusList.values(); + } + + // If no container exists, skip it + let composeList = JSON.parse(res.stdout.toString()); + if (composeList.length == 0) { + continue; + } + + // Converts the container state to composeListStatus, result of command: docker compose ls --format json ... + let statusCounter = new Map(); + for (let composeStack of composeList) { + statusCounter.set(composeStack.State, (statusCounter.get(composeStack.State) || 0) + 1); + } + + let statusStr = new Array(); + for (const key of statusCounter.keys()) { + statusStr.push(`${key}(${statusCounter.get(key)})`); + } + + let status: composeListStatus = { + Name: filename, + Status: statusStr.join(","), + ConfigFiles: composeFile, + }; + + statusList.push(status); + } catch (e) { + if (e instanceof Error) { + log.warn("getStackList", `Failed to get stack ${filename}, error: ${e.message}`); + } + } + } + + return statusList.values(); + } /** * 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 async getStatusList() : Promise> { + static async getStatusList(server: DockgeServer) : Promise> { let statusList = new Map(); - - let res = await childProcessAsync.spawn("docker", [ "compose", "ls", "--all", "--format", "json" ], { - encoding: "utf-8", - }); - - if (!res.stdout) { - return statusList; - } - - let composeList = JSON.parse(res.stdout.toString()); + let composeList = await this.getStackComposeList(server); for (let composeStack of composeList) { statusList.set(composeStack.Name, this.statusConvert(composeStack.Status)); @@ -409,7 +470,7 @@ export class Stack { async start(socket: DockgeSocket) { const terminalName = getComposeTerminalName(socket.endpoint, this.name); - let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "up", "-d", "--remove-orphans" ], this.path); + let exitCode = await Terminal.exec(this.server, socket, terminalName,this.server.dockerPath, [ "compose", "up", "-d", "--remove-orphans" ], this.path); if (exitCode !== 0) { throw new Error("Failed to start, please check the terminal output for more information."); } @@ -418,7 +479,7 @@ export class Stack { async stop(socket: DockgeSocket) : Promise { const terminalName = getComposeTerminalName(socket.endpoint, this.name); - let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "stop" ], this.path); + let exitCode = await Terminal.exec(this.server, socket, terminalName, this.server.dockerPath, [ "compose", "stop" ], this.path); if (exitCode !== 0) { throw new Error("Failed to stop, please check the terminal output for more information."); } @@ -427,7 +488,7 @@ export class Stack { async restart(socket: DockgeSocket) : Promise { const terminalName = getComposeTerminalName(socket.endpoint, this.name); - let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "restart" ], this.path); + let exitCode = await Terminal.exec(this.server, socket, terminalName, this.server.dockerPath, [ "compose", "restart" ], this.path); if (exitCode !== 0) { throw new Error("Failed to restart, please check the terminal output for more information."); } @@ -436,7 +497,7 @@ export class Stack { async down(socket: DockgeSocket) : Promise { const terminalName = getComposeTerminalName(socket.endpoint, this.name); - let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "down" ], this.path); + let exitCode = await Terminal.exec(this.server, socket, terminalName, this.server.dockerPath, [ "compose", "down" ], this.path); if (exitCode !== 0) { throw new Error("Failed to down, please check the terminal output for more information."); } @@ -445,19 +506,19 @@ export class Stack { async update(socket: DockgeSocket) { const terminalName = getComposeTerminalName(socket.endpoint, this.name); - let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "pull" ], this.path); + let exitCode = await Terminal.exec(this.server, socket, terminalName, this.server.dockerPath, [ "compose", "pull" ], this.path); if (exitCode !== 0) { throw new Error("Failed to pull, please check the terminal output for more information."); } // If the stack is not running, we don't need to restart it - await this.updateStatus(); + await this.updateStatus(this.server); log.debug("update", "Status: " + this.status); if (this.status !== RUNNING) { return exitCode; } - exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "up", "-d", "--remove-orphans" ], this.path); + exitCode = await Terminal.exec(this.server, socket, terminalName, this.server.dockerPath, [ "compose", "up", "-d", "--remove-orphans" ], this.path); if (exitCode !== 0) { throw new Error("Failed to restart, please check the terminal output for more information."); } @@ -466,7 +527,7 @@ export class Stack { async joinCombinedTerminal(socket: DockgeSocket) { const terminalName = getCombinedTerminalName(socket.endpoint, this.name); - const terminal = Terminal.getOrCreateTerminal(this.server, terminalName, "docker", [ "compose", "logs", "-f", "--tail", "100" ], this.path); + const terminal = Terminal.getOrCreateTerminal(this.server, terminalName, this.server.dockerPath, [ "compose", "logs", "-f", "--tail", "100" ], this.path); terminal.enableKeepAlive = true; terminal.rows = COMBINED_TERMINAL_ROWS; terminal.cols = COMBINED_TERMINAL_COLS; @@ -487,7 +548,7 @@ export class Stack { let terminal = Terminal.getTerminal(terminalName); if (!terminal) { - terminal = new InteractiveTerminal(this.server, terminalName, "docker", [ "compose", "exec", serviceName, shell ], this.path); + terminal = new InteractiveTerminal(this.server, terminalName, this.server.dockerPath, [ "compose", "exec", serviceName, shell ], this.path); terminal.rows = TERMINAL_ROWS; log.debug("joinContainerTerminal", "Terminal created"); } @@ -500,7 +561,7 @@ export class Stack { let statusList = new Map(); try { - let res = await childProcessAsync.spawn("docker", [ "compose", "ps", "--format", "json" ], { + let res = await childProcessAsync.spawn(this.server.dockerPath, [ "compose", "ps", "--format", "json" ], { cwd: this.path, encoding: "utf-8", }); diff --git a/backend/util-server.ts b/backend/util-server.ts index 227ece00..994c7bac 100644 --- a/backend/util-server.ts +++ b/backend/util-server.ts @@ -30,6 +30,7 @@ export interface Arguments { hostname? : string; dataDir? : string; stacksDir? : string; + dockerPath? : string; } // Some config values are required diff --git a/common/util-common.ts b/common/util-common.ts index 587e6dd2..ff77e4a0 100644 --- a/common/util-common.ts +++ b/common/util-common.ts @@ -109,6 +109,7 @@ export const ERROR_TYPE_VALIDATION = 1; export const allowedCommandList : string[] = [ "docker", + "nerdctl", "ls", "cd", "dir", diff --git a/compose.yaml b/compose.yaml index b2b7bdb0..4c23e91e 100644 --- a/compose.yaml +++ b/compose.yaml @@ -20,3 +20,4 @@ services: environment: # Tell Dockge where is your stacks directory - DOCKGE_STACKS_DIR=/opt/stacks + - DOCKGE_DOCKER_PATH=docker # or nerdctl