From 8b8f478d551907061c554ec853de52bdb6abd4e6 Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 13 Dec 2024 22:12:44 -0500 Subject: [PATCH] Add discovery search for projects within stacks directory that are not known to docker compose --- backend/stack.ts | 60 ++++++++++++++++++++++++++++++++++++++++--- common/util-common.ts | 11 ++++++++ 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/backend/stack.ts b/backend/stack.ts index 2e29591d..0e7118ec 100644 --- a/backend/stack.ts +++ b/backend/stack.ts @@ -6,6 +6,8 @@ import { DockgeSocket, fileExists, ValidationError } from "./util-server"; import path from "path"; import { acceptedComposeFileNames, + acceptedComposeFileNamePattern, + ArbitrarilyNestedLooseObject, COMBINED_TERMINAL_COLS, COMBINED_TERMINAL_ROWS, CREATED_FILE, @@ -271,7 +273,7 @@ export class Stack { return stackList; } - // Get status from docker compose ls + // Get stacks from docker compose ls let res = await childProcessAsync.spawn("docker", [ "compose", "ls", "--all", "--format", "json" ], { encoding: "utf-8", }); @@ -282,6 +284,7 @@ export class Stack { } let composeList = JSON.parse(res.stdout.toString()); + let pathSearchTree: ArbitrarilyNestedLooseObject = {}; // search tree for matching paths for (let composeStack of composeList) { try { @@ -292,11 +295,63 @@ export class Stack { stack._configFilePath = path.dirname(composeFiles[0]); stack._composeFileName = path.basename(composeFiles[0]); stackList.set(composeStack.Name, stack); + + // add project path to search tree so we can quickly determine if we have seen it before + // e.g. the path "/opt/stacks/project" yields the tree "{ opt: { stacks: { project: {} } } }" + path.join(stack._configFilePath, stack._composeFileName).split(path.sep).reduce((searchTree, pathComponent) => { + if (pathComponent == "") { + return searchTree; + } + if (!searchTree[pathComponent]) { + searchTree[pathComponent] = {}; + } + return searchTree; + }, pathSearchTree); } catch (e) { if (e instanceof Error) { - log.warn("getStackList", `Failed to get stack ${composeStack.Name}, error: ${e.message}`); + 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)); + for (let composeFile of acceptedComposeFiles) { + // check if we have seen this file before + let fullPath = path.join(server.stacksDir, 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 + let [ configFilePath, configFilename ] = [ path.dirname(fullPath), path.basename(fullPath) ]; + let stack = new Stack(server, configFilePath); + stack._status = UNKNOWN; + stack._configFilePath = configFilePath; + stack._composeFileName = configFilename; + stackList.set(configFilePath, stack); } } + } catch (e) { + if (e instanceof Error) { + log.error("getStackList", `Got error searching for undiscovered stacks:\n${e.message}`); + } } this.managedStackList = stackList; @@ -483,6 +538,5 @@ export class Stack { log.error("getServiceStatusList", e); return statusList; } - } } diff --git a/common/util-common.ts b/common/util-common.ts index 587e6dd2..b1ad83d9 100644 --- a/common/util-common.ts +++ b/common/util-common.ts @@ -21,6 +21,10 @@ export interface LooseObject { [key: string]: any } +export interface ArbitrarilyNestedLooseObject { + [key: string]: ArbitrarilyNestedLooseObject | Record; +} + export interface BaseRes { ok: boolean; msg?: string; @@ -125,6 +129,13 @@ export const acceptedComposeFileNames = [ "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 * @param str Input