import { AnyStackConfig, FilesOnServerConfig, GitStackConfig } from "../../common/infrastructure/config/stackConfig.js"; import { TomlStack } from "../../common/infrastructure/tomlObjects.js"; import { promises } from 'fs'; import { pathExistsAndIsReadable, readDirectories } from "../../common/utils/io.js"; import { childLogger, Logger } from "@foxxmd/logging"; import { isUndefinedOrEmptyString, parseBool } from "../../common/utils/utils.js"; import { detectGitRepo, GitRepoData, komodoRepoFromRemote, matchGitDataWithKomodo } from "../../common/utils/git.js"; import { buildGitStack } from "./gitStack.js"; import { join as joinPath, parse, ParsedPath } from 'path'; import { DEFAULT_COMPOSE_GLOB, DEFAULT_ENV_GLOB } from "./stackUtils.js"; import { buildFileStack } from "./filesOnServer.js"; import { SimpleError } from "../../common/errors.js"; export const buildStacksFromPath = async (path: string, options: AnyStackConfig, parentLogger: Logger): Promise => { const { composeFileGlob = DEFAULT_COMPOSE_GLOB, envFileGlob = DEFAULT_ENV_GLOB, } = options; let stackOptions = options; const logger = childLogger(parentLogger, 'Stacks'); let topDir: string; try { topDir = await promises.realpath(path); logger.info(`Top Dir: ${path} -> Resolved: ${topDir}`); pathExistsAndIsReadable(topDir) } catch (e) { throw new Error(`Could not access ${path}.${parseBool(process.env.IS_DOCKER) ? ' This is the path *in container* that is read so make sure you have mounted it on the host!' : ''}`); } let stacksDir: string = topDir; if (!isUndefinedOrEmptyString(process.env.GIT_STACKS_DIR)) { try { stacksDir = await promises.realpath(process.env.GIT_STACKS_DIR); logger.info(`Git Stack Dir: ${stacksDir} -> Resolved: ${stacksDir}`); pathExistsAndIsReadable(stacksDir) } catch (e) { throw new Error(`Could not access ${stacksDir}.${parseBool(process.env.IS_DOCKER) ? ' This is the path *in container* that is read so make sure you have mounted it on the host!' : ''}`); } } let stacks: TomlStack[] = []; const dirs = await readDirectories(stacksDir); const folderPaths = dirs.map(x => joinPath(stacksDir, x)); let gitData: GitRepoData; try { gitData = await detectGitRepo(path); } catch (e) { if (e instanceof SimpleError) { logger.debug(e.message); } else { throw e; } } logger.info(`Processing Stacks for ${dirs.length} folders in ${stacksDir}:\n${dirs.join('\n')}`); logger.info(`Compose File Glob: ${composeFileGlob}`); logger.info(`Env Glob: ${envFileGlob}`); let hostParentPathVerified = false; if (gitData !== undefined) { logger.info(`Detected top-level dir ${topDir} is a Git repo: Branch ${gitData[0].branch} | Remote ${gitData[1].remote} | URL ${gitData[1].url}`); logger.info('Will treat this as a monorepo -- all subfolders will be built as Git-Repo Stacks using the same repo with Run Directory relative to repo root.'); let gitStackConfig: Partial = { inMonorepo: true } try { const [provider, linkedRepo, repoHint] = await matchGitDataWithKomodo(gitData); if (repoHint !== undefined) { logger.warn(`All Stacks will be built without a linked repo: ${repoHint}`); } const [domain, repo] = komodoRepoFromRemote(gitData[1].url) if (repo === undefined) { gitStackConfig = { ...options, git_provider: provider?.domain ?? domain, git_account: provider?.username, repo }; } else { gitStackConfig = { ...options, linked_repo: linkedRepo.name, } } } catch (e) { throw new Error('Cannot use top-level git for Stacks', { cause: e }); } stackOptions = { ...stackOptions, ...gitStackConfig, writeEnv: parseBool(process.env.GIT_WRITE_ENV, false), inMonorepo: true } } else { logger.info('Top-level dir is not a git repo -- subfolders will be individually detected as Git repo or files-on-server Stacks.'); } for (const f of folderPaths) { const pathInfo: ParsedPath = parse(f); const folderLogger = childLogger(logger, `${pathInfo.name}${pathInfo.ext !== '' ? pathInfo.ext : ''}`); if (gitData !== undefined) { try { stacks.push(await buildGitStack(f, { ...stackOptions, logger, hostParentPath: stacksDir === topDir ? undefined : stacksDir.replace(topDir, '').replace(/^\//, '') })); } catch (e) { folderLogger.error(new Error(`Unable to build Git Stack for folder ${f}`, { cause: e })); } } else { try { stacks.push(await buildGitStack(f, { inMonorepo: false, writeEnv: parseBool(process.env.GIT_WRITE_ENV, false), ...options, logger })); continue; } catch (e) { if (e instanceof SimpleError) { if (e.message.includes('does not have a .git folder')) { folderLogger.debug('Falling back to Files-On-Server, not a git repo'); } else { folderLogger.verbose(`Falling back to Files-On-Server => ${e.message}`); } } else { folderLogger.error(new Error(`Unable to build Git Stack for folder ${f}`, { cause: e })); continue; } } const opts = options as FilesOnServerConfig; if (!hostParentPathVerified) { if (isUndefinedOrEmptyString(opts.hostParentPath)) { throw new Error('env HOST_PARENT_PATH is not set'); } hostParentPathVerified = true; } try { stacks.push(await buildFileStack(f, { ...opts, logger })); } catch (e) { folderLogger.error(new Error(`Unable to build Files-On-Server Stack for folder ${f}`, { cause: e })); } } } return stacks; }