Skip to content

Commit 2c29aea

Browse files
committed
Add discovery search for projects within stacks directory that are not
known to docker compose
1 parent fc4ad7f commit 2c29aea

File tree

2 files changed

+73
-2
lines changed

2 files changed

+73
-2
lines changed

backend/stack.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { DockgeSocket, fileExists, ValidationError } from "./util-server";
66
import path from "path";
77
import {
88
acceptedComposeFileNames,
9+
acceptedComposeFileNamePattern,
10+
ArbitrarilyNestedLooseObject,
911
COMBINED_TERMINAL_COLS,
1012
COMBINED_TERMINAL_ROWS,
1113
CREATED_FILE,
@@ -282,6 +284,7 @@ export class Stack {
282284
}
283285

284286
let composeList = JSON.parse(res.stdout.toString());
287+
let pathSearchTree: ArbitrarilyNestedLooseObject = {}; // search structure for matching paths
285288

286289
for (let composeStack of composeList) {
287290
try {
@@ -296,11 +299,69 @@ export class Stack {
296299
continue;
297300
}
298301
stackList.set(composeStack.Name, stack);
302+
303+
// add project path to search tree so we can quickly decide if we have seen it before later
304+
// e.g. path "/opt/stacks" would yield the tree { opt: stacks: {} }
305+
path.join(stack._configFilePath, stack._composeFileName).split(path.sep).reduce((searchTree, pathComponent) => {
306+
if (pathComponent == "") {
307+
return searchTree;
308+
}
309+
if (!searchTree[pathComponent]) {
310+
searchTree[pathComponent] = {};
311+
}
312+
return searchTree[pathComponent];
313+
}, pathSearchTree);
299314
} catch (e) {
300315
if (e instanceof Error) {
301-
log.warn("getStackList", `Failed to get stack ${composeStack.Name}, error: ${e.message}`);
316+
log.error("getStackList", `Failed to get stack ${composeStack.Name}, error: ${e.message}`);
317+
}
318+
}
319+
}
320+
321+
// Search stacks directory for compose files not associated with a running compose project (ie. never started through CLI)
322+
try {
323+
// 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.
324+
let rawFilesList = fs.readdirSync(server.stacksDir, {
325+
recursive: true,
326+
withFileTypes: true
327+
});
328+
let acceptedComposeFiles = rawFilesList.filter((dirEnt: fs.Dirent) => dirEnt.isFile() && !!dirEnt.name.match(acceptedComposeFileNamePattern));
329+
log.debug("getStackList", `Folder scan yielded ${acceptedComposeFiles.length} files`);
330+
for (let composeFile of acceptedComposeFiles) {
331+
// check if we have seen this file before
332+
let fullPath = composeFile.parentPath;
333+
let previouslySeen = fullPath.split(path.sep).reduce((searchTree: ArbitrarilyNestedLooseObject | boolean, pathComponent) => {
334+
if (pathComponent == "") {
335+
return searchTree;
336+
}
337+
338+
// end condition
339+
if (searchTree == false || !(searchTree as ArbitrarilyNestedLooseObject)[pathComponent]) {
340+
return false;
341+
}
342+
343+
// path (so far) has been previously seen
344+
return (searchTree as ArbitrarilyNestedLooseObject)[pathComponent];
345+
}, pathSearchTree);
346+
if (!previouslySeen) {
347+
// 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
348+
log.info("getStackList", `Found project unknown to docker compose: ${fullPath}/${composeFile.name}`);
349+
let [ configFilePath, configFilename, inferredProjectName ] = [ fullPath, composeFile.name, path.basename(fullPath) ];
350+
if (stackList.get(inferredProjectName)) {
351+
log.info("getStackList", `... but it was ignored. A project named ${inferredProjectName} already exists`);
352+
} else {
353+
let stack = new Stack(server, inferredProjectName);
354+
stack._status = UNKNOWN;
355+
stack._configFilePath = configFilePath;
356+
stack._composeFileName = configFilename;
357+
stackList.set(inferredProjectName, stack);
358+
}
302359
}
303360
}
361+
} catch (e) {
362+
if (e instanceof Error) {
363+
log.error("getStackList", `Got error searching for undiscovered stacks:\n${e.message}`);
364+
}
304365
}
305366

306367
this.managedStackList = stackList;
@@ -492,6 +553,5 @@ export class Stack {
492553
log.error("getServiceStatusList", e);
493554
return statusList;
494555
}
495-
496556
}
497557
}

common/util-common.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ export interface LooseObject {
2121
[key: string]: any
2222
}
2323

24+
export interface ArbitrarilyNestedLooseObject {
25+
[key: string]: ArbitrarilyNestedLooseObject | Record<string, never>;
26+
}
27+
2428
export interface BaseRes {
2529
ok: boolean;
2630
msg?: string;
@@ -125,6 +129,13 @@ export const acceptedComposeFileNames = [
125129
"compose.yml",
126130
];
127131

132+
// Make a regex out of accepted compose file names
133+
export const acceptedComposeFileNamePattern = new RegExp(
134+
acceptedComposeFileNames
135+
.map((filename: string) => filename.replace(".", "\\$&"))
136+
.join("|")
137+
);
138+
128139
/**
129140
* Generate a decimal integer number from a string
130141
* @param str Input

0 commit comments

Comments
 (0)