Skip to content

Commit 33de00a

Browse files
committed
Add discovery search for projects within stacks directory that are not
known to docker compose
1 parent 5115032 commit 33de00a

File tree

2 files changed

+74
-3
lines changed

2 files changed

+74
-3
lines changed

backend/stack.ts

Lines changed: 63 additions & 3 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,
@@ -271,7 +273,7 @@ export class Stack {
271273
return stackList;
272274
}
273275

274-
// Get status from docker compose ls
276+
// Get stacks from docker compose ls
275277
let res = await childProcessAsync.spawn("docker", [ "compose", "ls", "--all", "--format", "json" ], {
276278
encoding: "utf-8",
277279
});
@@ -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 {
@@ -292,11 +295,69 @@ export class Stack {
292295
stack._configFilePath = path.dirname(composeFiles[0]);
293296
stack._composeFileName = path.basename(composeFiles[0]);
294297
stackList.set(composeStack.Name, stack);
298+
299+
// add project path to search structure to use later
300+
// e.g. path "/opt/stacks" would yield the tree { opt: stacks: {} }
301+
path.join(stack._configFilePath, stack._composeFileName).split(path.sep).reduce((searchTree, pathComponent) => {
302+
if (pathComponent == "") {
303+
return searchTree;
304+
}
305+
if (!searchTree[pathComponent]) {
306+
searchTree[pathComponent] = {};
307+
}
308+
return searchTree[pathComponent];
309+
}, pathSearchTree);
295310
} catch (e) {
296311
if (e instanceof Error) {
297-
log.warn("getStackList", `Failed to get stack ${composeStack.Name}, error: ${e.message}`);
312+
log.error("getStackList", `Failed to get stack ${composeStack.Name}, error: ${e.message}`);
313+
}
314+
}
315+
}
316+
317+
// Search stacks directory for compose files not associated with a running compose project (ie. never started through CLI)
318+
try {
319+
// 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.
320+
let rawFilesList = fs.readdirSync(server.stacksDir, {
321+
recursive: true,
322+
withFileTypes: true
323+
});
324+
let acceptedComposeFiles = rawFilesList.filter((dirEnt: fs.Dirent) => dirEnt.isFile() && !!dirEnt.name.match(acceptedComposeFileNamePattern));
325+
log.debug("getStackList", `Folder scan yielded ${acceptedComposeFiles.length} files`);
326+
for (let composeFile of acceptedComposeFiles) {
327+
// check if we have seen this file before
328+
let fullPath = composeFile.parentPath;
329+
let previouslySeen = fullPath.split(path.sep).reduce((searchTree: ArbitrarilyNestedLooseObject | boolean, pathComponent) => {
330+
if (pathComponent == "") {
331+
return searchTree;
332+
}
333+
334+
// end condition
335+
if (searchTree == false || !(searchTree as ArbitrarilyNestedLooseObject)[pathComponent]) {
336+
return false;
337+
}
338+
339+
// path (so far) has been previously seen
340+
return (searchTree as ArbitrarilyNestedLooseObject)[pathComponent];
341+
}, pathSearchTree);
342+
if (!previouslySeen) {
343+
// 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
344+
log.info("getStackList", `Found project unknown to docker compose: ${fullPath}/${composeFile.name}`);
345+
let [ configFilePath, configFilename, inferredProjectName ] = [ fullPath, composeFile.name, path.basename(fullPath) ];
346+
if (stackList.get(inferredProjectName)) {
347+
log.info("getStackList", `... but it was ignored. A project named ${inferredProjectName} already exists`);
348+
} else {
349+
let stack = new Stack(server, inferredProjectName);
350+
stack._status = UNKNOWN;
351+
stack._configFilePath = configFilePath;
352+
stack._composeFileName = configFilename;
353+
stackList.set(inferredProjectName, stack);
354+
}
298355
}
299356
}
357+
} catch (e) {
358+
if (e instanceof Error) {
359+
log.error("getStackList", `Got error searching for undiscovered stacks:\n${e.message}`);
360+
}
300361
}
301362

302363
this.managedStackList = stackList;
@@ -483,6 +544,5 @@ export class Stack {
483544
log.error("getServiceStatusList", e);
484545
return statusList;
485546
}
486-
487547
}
488548
}

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)