Compare commits

...

4 commits

Author SHA1 Message Date
Martin Koo
3578a1970e
Merge 2c29aea921 into d451e06e84 2025-01-04 04:24:36 -08:00
Louis Lam
d451e06e84 Update dependencies
Some checks failed
Node.js CI - Dockge / ci (22, ARM) (push) Has been cancelled
Node.js CI - Dockge / ci (22, ARM64) (push) Has been cancelled
Node.js CI - Dockge / ci (22, macos-latest) (push) Has been cancelled
Node.js CI - Dockge / ci (22, ubuntu-latest) (push) Has been cancelled
Node.js CI - Dockge / ci (22, windows-latest) (push) Has been cancelled
json-yaml-validate / json-yaml-validate (push) Has been cancelled
2025-01-04 18:14:17 +08:00
Martin
2c29aea921
Add discovery search for projects within stacks directory that are not
known to docker compose
2024-12-14 21:32:54 -05:00
Martin
fc4ad7ff29
draft support for nested stacks directory 2024-12-14 21:32:50 -05:00
4 changed files with 354 additions and 237 deletions

View file

@ -6,6 +6,8 @@ import { DockgeSocket, fileExists, ValidationError } from "./util-server";
import path from "path"; import path from "path";
import { import {
acceptedComposeFileNames, acceptedComposeFileNames,
acceptedComposeFileNamePattern,
ArbitrarilyNestedLooseObject,
COMBINED_TERMINAL_COLS, COMBINED_TERMINAL_COLS,
COMBINED_TERMINAL_ROWS, COMBINED_TERMINAL_ROWS,
CREATED_FILE, CREATED_FILE,
@ -104,7 +106,7 @@ export class Stack {
} }
get isManagedByDockge() : boolean { get isManagedByDockge() : boolean {
return fs.existsSync(this.path) && fs.statSync(this.path).isDirectory(); return !!this._configFilePath && this._configFilePath.startsWith(this.server.stacksDir);
} }
get status() : number { get status() : number {
@ -153,7 +155,7 @@ export class Stack {
} }
get path() : string { get path() : string {
return path.join(this.server.stacksDir, this.name); return this._configFilePath || "";
} }
get fullPath() : string { get fullPath() : string {
@ -263,41 +265,12 @@ export class Stack {
} }
static async getStackList(server : DockgeServer, useCacheForManaged = false) : Promise<Map<string, Stack>> { static async getStackList(server : DockgeServer, useCacheForManaged = false) : Promise<Map<string, Stack>> {
let stacksDir = server.stacksDir; let stackList : Map<string, Stack> = new Map<string, Stack>();
let stackList : Map<string, Stack>;
// Use cached stack list? // Use cached stack list?
if (useCacheForManaged && this.managedStackList.size > 0) { if (useCacheForManaged && this.managedStackList.size > 0) {
stackList = this.managedStackList; stackList = this.managedStackList;
} else { return stackList;
stackList = new Map<string, Stack>();
// Scan the stacks directory, and get the stack list
let filenameList = await fsAsync.readdir(stacksDir);
for (let filename of filenameList) {
try {
// Check if it is a directory
let stat = await fsAsync.stat(path.join(stacksDir, filename));
if (!stat.isDirectory()) {
continue;
}
// If no compose file exists, skip it
if (!await Stack.composeFileExists(stacksDir, filename)) {
continue;
}
let stack = await this.getStack(server, filename);
stack._status = CREATED_FILE;
stackList.set(filename, stack);
} catch (e) {
if (e instanceof Error) {
log.warn("getStackList", `Failed to get stack ${filename}, error: ${e.message}`);
}
}
}
// Cache by copying
this.managedStackList = new Map(stackList);
} }
// Get status from docker compose ls // Get status from docker compose ls
@ -306,28 +279,92 @@ export class Stack {
}); });
if (!res.stdout) { if (!res.stdout) {
log.warn("getStackList", "No response from docker compose daemon when attempting to retrieve list of stacks");
return stackList; return stackList;
} }
let composeList = JSON.parse(res.stdout.toString()); let composeList = JSON.parse(res.stdout.toString());
let pathSearchTree: ArbitrarilyNestedLooseObject = {}; // search structure for matching paths
for (let composeStack of composeList) { for (let composeStack of composeList) {
let stack = stackList.get(composeStack.Name); try {
let stack = new Stack(server, composeStack.Name);
stack._status = this.statusConvert(composeStack.Status);
// This stack probably is not managed by Dockge, but we still want to show it let composeFiles = composeStack.ConfigFiles.split(","); // it is possible for a project to have more than one config file
if (!stack) { stack._configFilePath = path.dirname(composeFiles[0]);
// Skip the dockge stack if it is not managed by Dockge stack._composeFileName = path.basename(composeFiles[0]);
if (composeStack.Name === "dockge") { if (stack.name === "dockge" && !stack.isManagedByDockge) {
// skip dockge if not managed by dockge
continue; continue;
} }
stack = new Stack(server, composeStack.Name);
stackList.set(composeStack.Name, stack); stackList.set(composeStack.Name, stack);
}
stack._status = this.statusConvert(composeStack.Status); // add project path to search tree so we can quickly decide if we have seen it before later
stack._configFilePath = composeStack.ConfigFiles; // e.g. path "/opt/stacks" would yield the tree { opt: stacks: {} }
path.join(stack._configFilePath, stack._composeFileName).split(path.sep).reduce((searchTree, pathComponent) => {
if (pathComponent == "") {
return searchTree;
}
if (!searchTree[pathComponent]) {
searchTree[pathComponent] = {};
}
return searchTree[pathComponent];
}, pathSearchTree);
} catch (e) {
if (e instanceof Error) {
log.error("getStackList", `Failed to get stack ${composeStack.Name}, error: ${e.message}`);
}
}
} }
// Search stacks directory for compose files not associated with a running compose project (ie. never started through CLI)
try {
// Hopefully the user has access to everything in this directory! If they don't, log the error. It is a small price to pay for fast searching.
let rawFilesList = fs.readdirSync(server.stacksDir, {
recursive: true,
withFileTypes: true
});
let acceptedComposeFiles = rawFilesList.filter((dirEnt: fs.Dirent) => dirEnt.isFile() && !!dirEnt.name.match(acceptedComposeFileNamePattern));
log.debug("getStackList", `Folder scan yielded ${acceptedComposeFiles.length} files`);
for (let composeFile of acceptedComposeFiles) {
// check if we have seen this file before
let fullPath = composeFile.parentPath;
let previouslySeen = fullPath.split(path.sep).reduce((searchTree: ArbitrarilyNestedLooseObject | boolean, pathComponent) => {
if (pathComponent == "") {
return searchTree;
}
// end condition
if (searchTree == false || !(searchTree as ArbitrarilyNestedLooseObject)[pathComponent]) {
return false;
}
// path (so far) has been previously seen
return (searchTree as ArbitrarilyNestedLooseObject)[pathComponent];
}, pathSearchTree);
if (!previouslySeen) {
// a file with an accepted compose filename has been found that did not appear in `docker compose ls`. Use its config file path as a temp name
log.info("getStackList", `Found project unknown to docker compose: ${fullPath}/${composeFile.name}`);
let [ configFilePath, configFilename, inferredProjectName ] = [ fullPath, composeFile.name, path.basename(fullPath) ];
if (stackList.get(inferredProjectName)) {
log.info("getStackList", `... but it was ignored. A project named ${inferredProjectName} already exists`);
} else {
let stack = new Stack(server, inferredProjectName);
stack._status = UNKNOWN;
stack._configFilePath = configFilePath;
stack._composeFileName = configFilename;
stackList.set(inferredProjectName, stack);
}
}
}
} catch (e) {
if (e instanceof Error) {
log.error("getStackList", `Got error searching for undiscovered stacks:\n${e.message}`);
}
}
this.managedStackList = stackList;
return stackList; return stackList;
} }
@ -375,35 +412,24 @@ export class Stack {
} }
static async getStack(server: DockgeServer, stackName: string, skipFSOperations = false) : Promise<Stack> { static async getStack(server: DockgeServer, stackName: string, skipFSOperations = false) : Promise<Stack> {
let dir = path.join(server.stacksDir, stackName); let stack: Stack | undefined;
if (!skipFSOperations) { if (!skipFSOperations) {
if (!await fileExists(dir) || !(await fsAsync.stat(dir)).isDirectory()) { let stackList = await this.getStackList(server, true);
// Maybe it is a stack managed by docker compose directly stack = stackList.get(stackName);
let stackList = await this.getStackList(server, true); if (!stack || !await fileExists(stack.path) || !(await fsAsync.stat(stack.path)).isDirectory() ) {
let stack = stackList.get(stackName); throw new ValidationError(`getStack; Stack ${stackName} not found`);
if (stack) {
return stack;
} else {
// Really not found
throw new ValidationError("Stack not found");
}
} }
} else { } else {
//log.debug("getStack", "Skip FS operations"); // search for known stack with this name
if (this.managedStackList) {
stack = this.managedStackList.get(stackName);
}
if (!this.managedStackList || !stack) {
stack = new Stack(server, stackName, undefined, undefined, true);
stack._status = UNKNOWN;
stack._configFilePath = path.resolve(server.stacksDir, stackName);
}
} }
let stack : Stack;
if (!skipFSOperations) {
stack = new Stack(server, stackName);
} else {
stack = new Stack(server, stackName, undefined, undefined, true);
}
stack._status = UNKNOWN;
stack._configFilePath = path.resolve(dir);
return stack; return stack;
} }
@ -522,12 +548,10 @@ export class Stack {
} catch (e) { } catch (e) {
} }
} }
return statusList; return statusList;
} catch (e) { } catch (e) {
log.error("getServiceStatusList", e); log.error("getServiceStatusList", e);
return statusList; return statusList;
} }
} }
} }

View file

@ -21,6 +21,10 @@ export interface LooseObject {
[key: string]: any [key: string]: any
} }
export interface ArbitrarilyNestedLooseObject {
[key: string]: ArbitrarilyNestedLooseObject | Record<string, never>;
}
export interface BaseRes { export interface BaseRes {
ok: boolean; ok: boolean;
msg?: string; msg?: string;
@ -125,6 +129,13 @@ export const acceptedComposeFileNames = [
"compose.yml", "compose.yml",
]; ];
// Make a regex out of accepted compose file names
export const acceptedComposeFileNamePattern = new RegExp(
acceptedComposeFileNames
.map((filename: string) => filename.replace(".", "\\$&"))
.join("|")
);
/** /**
* Generate a decimal integer number from a string * Generate a decimal integer number from a string
* @param str Input * @param str Input

View file

@ -40,7 +40,7 @@
"dotenv": "~16.3.2", "dotenv": "~16.3.2",
"express": "~4.21.2", "express": "~4.21.2",
"express-static-gzip": "~2.1.8", "express-static-gzip": "~2.1.8",
"http-graceful-shutdown": "~3.1.13", "http-graceful-shutdown": "~3.1.14",
"jsonwebtoken": "~9.0.2", "jsonwebtoken": "~9.0.2",
"jwt-decode": "~3.1.2", "jwt-decode": "~3.1.2",
"knex": "~2.5.1", "knex": "~2.5.1",
@ -49,8 +49,8 @@
"promisify-child-process": "~4.1.2", "promisify-child-process": "~4.1.2",
"redbean-node": "~0.3.3", "redbean-node": "~0.3.3",
"semver": "^7.6.3", "semver": "^7.6.3",
"socket.io": "~4.8.0", "socket.io": "~4.8.1",
"socket.io-client": "~4.8.0", "socket.io-client": "~4.8.1",
"timezones-list": "~3.0.3", "timezones-list": "~3.0.3",
"ts-command-line-args": "~2.5.1", "ts-command-line-args": "~2.5.1",
"tsx": "~4.19.2", "tsx": "~4.19.2",
@ -59,7 +59,7 @@
}, },
"devDependencies": { "devDependencies": {
"@actions/github": "^6.0.0", "@actions/github": "^6.0.0",
"@fontsource/jetbrains-mono": "^5.1.1", "@fontsource/jetbrains-mono": "^5.1.2",
"@fortawesome/fontawesome-svg-core": "6.4.2", "@fortawesome/fontawesome-svg-core": "6.4.2",
"@fortawesome/free-regular-svg-icons": "6.4.2", "@fortawesome/free-regular-svg-icons": "6.4.2",
"@fortawesome/free-solid-svg-icons": "6.4.2", "@fortawesome/free-solid-svg-icons": "6.4.2",
@ -81,19 +81,19 @@
"cross-env": "~7.0.3", "cross-env": "~7.0.3",
"eslint": "~8.50.0", "eslint": "~8.50.0",
"eslint-plugin-jsdoc": "~46.8.2", "eslint-plugin-jsdoc": "~46.8.2",
"eslint-plugin-vue": "~9.17.0", "eslint-plugin-vue": "~9.32.0",
"prismjs": "~1.29.0", "prismjs": "~1.29.0",
"sass": "~1.68.0", "sass": "~1.68.0",
"typescript": "~5.2.2", "typescript": "~5.2.2",
"unplugin-vue-components": "~0.25.2", "unplugin-vue-components": "~0.25.2",
"vite": "~5.4.8", "vite": "~5.4.11",
"vite-plugin-compression": "~0.5.1", "vite-plugin-compression": "~0.5.1",
"vue": "~3.5.12", "vue": "~3.5.13",
"vue-eslint-parser": "~9.3.2", "vue-eslint-parser": "~9.3.2",
"vue-i18n": "~9.5.0", "vue-i18n": "~10.0.5",
"vue-prism-editor": "2.0.0-alpha.2", "vue-prism-editor": "2.0.0-alpha.2",
"vue-qrcode": "~2.2.2", "vue-qrcode": "~2.2.2",
"vue-router": "~4.2.5", "vue-router": "~4.5.0",
"vue-toastification": "2.0.0-rc.5", "vue-toastification": "2.0.0-rc.5",
"wait-on": "^7.2.0", "wait-on": "^7.2.0",
"xterm-addon-web-links": "~0.9.0" "xterm-addon-web-links": "~0.9.0"

400
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff