@@ -6,6 +6,8 @@ import { DockgeSocket, fileExists, ValidationError } from "./util-server";
66import path from "path" ;
77import {
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}
0 commit comments