diff --git a/src/commands/foundation/TestModuleType.ts b/src/commands/foundation/TestModuleType.ts index 6a9b3b1c..c10cdd06 100644 --- a/src/commands/foundation/TestModuleType.ts +++ b/src/commands/foundation/TestModuleType.ts @@ -9,6 +9,7 @@ export class TestModuleType extends StringType { const excludes = { testModules: false, tenantModules: true, + platformModules: false, }; const modules = await repo.processFilesGlob( diff --git a/src/commands/foundation/deploy.command.ts b/src/commands/foundation/deploy.command.ts index fbd64539..080a8cc7 100644 --- a/src/commands/foundation/deploy.command.ts +++ b/src/commands/foundation/deploy.command.ts @@ -17,6 +17,7 @@ import { LiteralArgsParser } from "../LiteralArgsParser.ts"; import { TopLevelCommand } from "../TopLevelCommand.ts"; import { getCurrentWorkingFoundation } from "../../cli/commandOptionsConventions.ts"; import { NullProgressReporter } from "../../cli/NullProgressReporter.ts"; +import { FoundationDeployer } from "../../foundation/FoundationDeployer.ts"; interface DeployOptions { platform?: string; @@ -29,7 +30,7 @@ export function registerDeployCmd(program: TopLevelCommand) { const cmd = program .command("deploy [foundation:foundation]") .description( - "Deploy platform modules in your cloud foundations using terragrunt", + "Deploy foundation and platform modules in your cloud foundations using terragrunt", ) .type("module", new PlatformModuleType()) .option( @@ -121,6 +122,27 @@ export async function deployFoundation( const terragrunt = factory.buildTerragrunt(); + // unless targeting a specific platform, always deploy foundation modules + if (!opts.platform) { + const deployer = new FoundationDeployer( + repo, + foundation, + terragrunt, + logger, + ); + + await deployer.deployFoundationModules( + mode, + opts.module, + !!opts.autoApprove, + ); + + // if all we had to do was deploy a specific foundation module, we are done and can exit + if (opts.module) { + return; + } + } + const platforms = findPlatforms(opts.platform, foundation, logger); for (const platform of platforms) { diff --git a/src/commands/foundation/docs.command.ts b/src/commands/foundation/docs.command.ts index 214fe7c8..5f6134c0 100644 --- a/src/commands/foundation/docs.command.ts +++ b/src/commands/foundation/docs.command.ts @@ -1,21 +1,14 @@ -import * as fs from "std/fs"; import { CliApiFacadeFactory } from "../../api/CliApiFacadeFactory.ts"; import { Logger } from "../../cli/Logger.ts"; import { ProgressReporter } from "../../cli/ProgressReporter.ts"; -import { ComplianceControlRepository } from "../../compliance/ComplianceControlRepository.ts"; -import { ComplianceDocumentationGenerator } from "../../docs/ComplianceDocumentationGenerator.ts"; -import { DocumentationGenerator } from "../../docs/DocumentationGenerator.ts"; import { DocumentationRepository } from "../../docs/DocumentationRepository.ts"; -import { KitModuleDocumentationGenerator } from "../../docs/KitModuleDocumentationGenerator.ts"; -import { PlatformDocumentationGenerator } from "../../docs/PlatformDocumentationGenerator.ts"; -import { KitDependencyAnalyzer } from "../../kit/KitDependencyAnalyzer.ts"; -import { KitModuleRepository } from "../../kit/KitModuleRepository.ts"; import { CollieRepository } from "../../model/CollieRepository.ts"; import { FoundationRepository } from "../../model/FoundationRepository.ts"; import { ModelValidator } from "../../model/schemas/ModelValidator.ts"; import { GlobalCommandOptions } from "../GlobalCommandOptions.ts"; import { TopLevelCommand } from "../TopLevelCommand.ts"; import { getCurrentWorkingFoundation } from "../../cli/commandOptionsConventions.ts"; +import { exists } from "std/fs"; interface DocsCommandOptions { update?: boolean; @@ -89,59 +82,34 @@ export function registerDocsCmd(program: TopLevelCommand) { } async function updateDocumentation( - repo: CollieRepository, + _repo: CollieRepository, foundation: FoundationRepository, logger: Logger, ) { + const docsModulePath = foundation.resolvePath("docs"); + + if (!await exists(docsModulePath)) { + logger.error( + `Foundation-level docs module at "${docsModulePath}" does not exist.`, + ); + logger.tip( + "Import a starter docs module using 'collie kit import docs' command.", + ); + return; + } + const foundationProgress = new ProgressReporter( "generating docs", `foundation "${foundation.name}"`, logger, ); - const validator = new ModelValidator(logger); - const modules = await KitModuleRepository.load(repo, validator, logger); - const controls = await ComplianceControlRepository.load( - repo, - validator, - logger, - ); - const moduleDocumentation = new KitModuleDocumentationGenerator( - repo, - modules, - controls, - logger, - ); - - const complianceDocumentation = new ComplianceDocumentationGenerator( - repo, - logger, - ); - - const analyzer = new KitDependencyAnalyzer(repo, modules, logger); - const factory = new CliApiFacadeFactory(logger); const terragrunt = factory.buildTerragrunt(); - const platformDocumentation = new PlatformDocumentationGenerator( - repo, - foundation, - analyzer, - controls, - terragrunt, - logger, - ); - const docsRepo = new DocumentationRepository(foundation); - - await prepareSiteTemplate(docsRepo, repo, logger); - - const generator = new DocumentationGenerator( - moduleDocumentation, - complianceDocumentation, - platformDocumentation, - ); - - await generator.generateFoundationDocumentation(docsRepo); + await terragrunt.run(docsModulePath, { raw: ["apply"] }, { + autoApprove: true, + }); foundationProgress.done(); } @@ -171,34 +139,3 @@ async function buildDocumentation( await npm.run(["install"], { cwd: dir }); await npm.run(["run", "docs:build"], { cwd: dir }); } - -async function prepareSiteTemplate( - docsRepo: DocumentationRepository, - repo: CollieRepository, - logger: Logger, -) { - // TODO: throw if it doesn't work - const srcDir = repo.resolvePath("kit", "foundation", "docs", "template"); - - try { - await fs.copy(srcDir, docsRepo.resolvePath(), { overwrite: true }); - } catch (e) { - if (e instanceof Deno.errors.NotFound) { - logger.error( - (fmt) => - `could not find kit module with template for documentation site at ${ - fmt.kitPath( - srcDir, - ) - }`, - ); - - logger.tipCommand( - "This module is essential for documentation generation. To import this module run", - "kit import foundation/docs", - ); - Deno.exit(1); - } - throw e; - } -} diff --git a/src/commands/prepareAnalyzeCommand.ts b/src/commands/prepareAnalyzeCommand.ts index bc617263..60e575e5 100644 --- a/src/commands/prepareAnalyzeCommand.ts +++ b/src/commands/prepareAnalyzeCommand.ts @@ -1,4 +1,5 @@ import { Logger } from "../cli/Logger.ts"; +import { ComplianceControlRepository } from "../compliance/ComplianceControlRepository.ts"; import { FoundationDependencies, KitDependencyAnalyzer, @@ -32,11 +33,22 @@ async function analyze( const validator = new ModelValidator(logger); const modules = await KitModuleRepository.load(collie, validator, logger); + const controls = await ComplianceControlRepository.load( + collie, + validator, + logger, + ); + const foundations = await collie.listFoundations(); const tasks = foundations.map(async (f) => { const foundation = await FoundationRepository.load(collie, f, validator); - const analyzer = new KitDependencyAnalyzer(collie, modules, logger); + const analyzer = new KitDependencyAnalyzer( + collie, + modules, + controls, + logger, + ); return { foundation, diff --git a/src/docs/ComplianceDocumentationGenerator.ts b/src/docs/ComplianceDocumentationGenerator.ts deleted file mode 100644 index 7148bcb6..00000000 --- a/src/docs/ComplianceDocumentationGenerator.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as fs from "std/fs"; -import * as path from "std/path"; -import { Logger } from "../cli/Logger.ts"; -import { CollieRepository } from "../model/CollieRepository.ts"; -import { DocumentationRepository } from "./DocumentationRepository.ts"; - -export class ComplianceDocumentationGenerator { - constructor( - private readonly collie: CollieRepository, - private readonly logger: Logger, - ) {} - - public async generate(docsRepo: DocumentationRepository) { - const source = this.collie.resolvePath("compliance"); - - const destinationDir = docsRepo.resolveCompliancePath(); - this.logger.verbose( - (fmt) => - `copying (recursive) ${fmt.kitPath(source)} to ${ - fmt.kitPath( - destinationDir, - ) - }`, - ); - await Deno.mkdir(path.dirname(destinationDir), { recursive: true }); - await fs.copy(source, destinationDir, { overwrite: true }); - } -} diff --git a/src/docs/DocumentationGenerator.ts b/src/docs/DocumentationGenerator.ts deleted file mode 100644 index 04e1b411..00000000 --- a/src/docs/DocumentationGenerator.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ComplianceDocumentationGenerator } from "./ComplianceDocumentationGenerator.ts"; -import { DocumentationRepository } from "./DocumentationRepository.ts"; -import { KitModuleDocumentationGenerator } from "./KitModuleDocumentationGenerator.ts"; -import { PlatformDocumentationGenerator } from "./PlatformDocumentationGenerator.ts"; - -export class DocumentationGenerator { - constructor( - private readonly kitModuleDocumentation: KitModuleDocumentationGenerator, - private readonly complianceDocumentation: ComplianceDocumentationGenerator, - private readonly platformDocumentation: PlatformDocumentationGenerator, - ) {} - - async generateFoundationDocumentation(docsRepo: DocumentationRepository) { - // todo: can we flatten the duplicate docs/ folder nesting? - await this.complianceDocumentation.generate(docsRepo); - await this.kitModuleDocumentation.generate(docsRepo); - await this.platformDocumentation.generate(docsRepo); - } -} diff --git a/src/docs/DocumentationRepository.ts b/src/docs/DocumentationRepository.ts index 617e69a3..b124f9e0 100644 --- a/src/docs/DocumentationRepository.ts +++ b/src/docs/DocumentationRepository.ts @@ -7,7 +7,7 @@ export class DocumentationRepository { // we use a "hidden" directory with a leading "." because terragrunt excludes hidden files and dirs // when building a terragrunt-cache folder, see https://terragrunt.gruntwork.io/docs/reference/config-blocks-and-attributes/#terraform "include_in_copy" // > By default, Terragrunt excludes hidden files and folders during the copy step. - private readonly docsRootDir = ".docs"; + private readonly docsRootDir = ".docs-v2"; private readonly docsContentDir = "docs"; constructor(private readonly foundation: FoundationRepository) {} diff --git a/src/docs/KitModuleDocumentationGenerator.ts b/src/docs/KitModuleDocumentationGenerator.ts deleted file mode 100644 index c101c9ce..00000000 --- a/src/docs/KitModuleDocumentationGenerator.ts +++ /dev/null @@ -1,102 +0,0 @@ -import * as fs from "std/fs"; -import * as path from "std/path"; - -import { Logger } from "../cli/Logger.ts"; -import { ProgressReporter } from "../cli/ProgressReporter.ts"; -import { ComplianceControlRepository } from "../compliance/ComplianceControlRepository.ts"; -import { KitModuleRepository } from "../kit/KitModuleRepository.ts"; -import { ParsedKitModule } from "../kit/ParsedKitModule.ts"; -import { CollieRepository } from "../model/CollieRepository.ts"; -import { DocumentationRepository } from "./DocumentationRepository.ts"; - -export class KitModuleDocumentationGenerator { - constructor( - private readonly collie: CollieRepository, - private readonly kitModules: KitModuleRepository, - private readonly controls: ComplianceControlRepository, - private readonly logger: Logger, - ) {} - - async generate(docsRepo: DocumentationRepository) { - const progress = new ProgressReporter( - "generating", - "kit module documentation", - this.logger, - ); - - // generate all kit module READMEs - const tasks = this.kitModules.all.map(async (x) => { - const dest = docsRepo.resolveKitModulePath(x.id); - - this.logger.verbose((fmt) => `generating ${fmt.kitPath(dest)}`); - - const md = this.generateModuleDocumentation(x, docsRepo); - - await Deno.mkdir(path.dirname(dest), { recursive: true }); - await Deno.writeTextFile(dest, md); - }); - - tasks.push(this.copyTopLevelKitReamde(docsRepo)); - - await Promise.all(tasks); - - progress.done(); - } - - private async copyTopLevelKitReamde(docsRepo: DocumentationRepository) { - const source = this.collie.resolvePath("kit", "README.md"); - const dest = docsRepo.resolveKitModulePath("README"); - await fs.ensureDir(path.dirname(dest)); - await fs.copy(source, dest, { overwrite: true }); - } - - private generateModuleDocumentation( - parsed: ParsedKitModule, - docsRepo: DocumentationRepository, - ) { - const complianceStatements = this.generateComplianceStatements( - parsed, - docsRepo, - ); - - if (!complianceStatements?.length) { - return parsed.readme; // return verbatim - } - - return `${parsed.readme} - -## Compliance Statements - -${complianceStatements.filter((x) => !!x).join("\n")} -`; - } - - private generateComplianceStatements( - parsed: ParsedKitModule, - docsRepo: DocumentationRepository, - ) { - return parsed.kitModule.compliance?.map((x) => { - const control = this.controls.tryFindById(x.control); - if (!control) { - this.logger.warn( - `could not find compliance control ${x.control} referenced in a compliance statement in ${parsed.definitionPath}`, - ); - - return; - } - - return ` -### ${control.name} - -${x.statement} - -[${control.name}](${ - docsRepo.controlLink( - docsRepo.resolveKitModulePath(parsed.id), - x.control, - ) - }) -`; - }); - } -} diff --git a/src/docs/PlatformDocumentationGenerator.ts b/src/docs/PlatformDocumentationGenerator.ts deleted file mode 100644 index 4c49c0d0..00000000 --- a/src/docs/PlatformDocumentationGenerator.ts +++ /dev/null @@ -1,202 +0,0 @@ -import * as fs from "std/fs"; -import * as path from "std/path"; -import { TerragruntCliFacade } from "../api/terragrunt/TerragruntCliFacade.ts"; -import { Logger } from "../cli/Logger.ts"; -import { ProgressReporter } from "../cli/ProgressReporter.ts"; -import { - KitDependencyAnalyzer, - KitModuleDependency, - PlatformDependencies, -} from "../kit/KitDependencyAnalyzer.ts"; -import { CollieRepository } from "../model/CollieRepository.ts"; - -import { FoundationRepository } from "../model/FoundationRepository.ts"; -import { PlatformConfig } from "../model/PlatformConfig.ts"; -import { DocumentationRepository } from "./DocumentationRepository.ts"; -import { MarkdownUtils } from "../model/MarkdownUtils.ts"; -import { ComplianceControlRepository } from "../compliance/ComplianceControlRepository.ts"; -import { - RunIndividualPlatformModuleOutputCollector, -} from "./PlatformModuleOutputCollector.ts"; - -export class PlatformDocumentationGenerator { - constructor( - private readonly repo: CollieRepository, - private readonly foundation: FoundationRepository, - private readonly kitDependencyAnalyzer: KitDependencyAnalyzer, - private readonly controls: ComplianceControlRepository, - private readonly terragrunt: TerragruntCliFacade, - private readonly logger: Logger, - ) {} - - async generate(docsRepo: DocumentationRepository) { - await this.copyTopLevelPlatformsReamde(docsRepo); - await this.generatePlatformsDocumentation(docsRepo); - } - - private async copyTopLevelPlatformsReamde(docsRepo: DocumentationRepository) { - const source = this.foundation.resolvePath("platforms", "README.md"); - const dest = docsRepo.resolvePlatformsPath("README.md"); - - this.logger.verbose( - (fmt) => `Copying ${fmt.kitPath(source)} to ${fmt.kitPath(dest)}`, - ); - await fs.ensureDir(path.dirname(dest)); - await fs.copy(source, dest, { overwrite: true }); - } - - private async generatePlatformsDocumentation( - docsRepo: DocumentationRepository, - ) { - const foundationProgress = new ProgressReporter( - "generate documentation", - this.repo.relativePath(this.foundation.resolvePath()), - this.logger, - ); - - const foundationDependencies = await this.kitDependencyAnalyzer - .findKitModuleDependencies( - this.foundation, - ); - - for (const p of foundationDependencies.platforms) { - await this.generatePlatforDocumentation(p, docsRepo); - } - - foundationProgress.done(); - } - - private async generatePlatforDocumentation( - dependencies: PlatformDependencies, - docsRepo: DocumentationRepository, - ) { - const platformPath = this.foundation.resolvePlatformPath( - dependencies.platform, - ); - const platformProgress = new ProgressReporter( - "generate documentation", - this.repo.relativePath(platformPath), - this.logger, - ); - - const platformModuleDocumentation = - new RunIndividualPlatformModuleOutputCollector( - this.repo, - this.terragrunt, - this.logger, - ); - - // as a fallback process modules serially, unfortunately this is the only "safe" way to collect output - // see https://github.com/meshcloud/collie-cli/issues/265 - for (const dep of dependencies.modules) { - const documentationMd = await platformModuleDocumentation.getOutput(dep); - - await this.generatePlatformModuleDocumentation( - dep, - documentationMd, - docsRepo, - dependencies.platform, - ); - } - - platformProgress.done(); - } - - private async generatePlatformModuleDocumentation( - dep: KitModuleDependency, - documentationMd: string, - docsRepo: DocumentationRepository, - platform: PlatformConfig, - ) { - const destPath = docsRepo.resolvePlatformModulePath( - platform.id, - dep.kitModuleId, - ); - - await fs.ensureDir(path.dirname(destPath)); // todo: should we do nesting in the docs output or "flatten" module prefixes? - - const mdSections = [documentationMd]; - - const complianceStatementsBlock = this.generateComplianceStatementSection( - dep, - docsRepo, - destPath, - ); - mdSections.push(complianceStatementsBlock); - - const kitModuleSection = this.generateKitModuleSection( - dep, - docsRepo, - destPath, - ); - mdSections.push(kitModuleSection); - - await Deno.writeTextFile(destPath, mdSections.join("\n\n")); - - this.logger.verbose( - (fmt) => - `Wrote output "documentation_md" from platform module ${ - fmt.kitPath( - dep.sourcePath, - ) - } to ${fmt.kitPath(destPath)}`, - ); - } - - private generateKitModuleSection( - dep: KitModuleDependency, - docsRepo: DocumentationRepository, - destPath: string, - ) { - if (!dep.kitModule) { - return MarkdownUtils.container( - "warning", - "Invalid Kit Module Dependency", - "Could not find kit module at " + MarkdownUtils.code(dep.kitModulePath), - ); - } - - const kitModuleLink = MarkdownUtils.link( - dep.kitModule.name + " kit module", - docsRepo.kitModuleLink(destPath, dep.kitModuleId), - ); - - const kitModuleSection = `::: tip Kit module -This platform module is a deployment of kit module ${kitModuleLink}. -:::`; - return kitModuleSection; - } - - private generateComplianceStatementSection( - dep: KitModuleDependency, - docsRepo: DocumentationRepository, - destPath: string, - ) { - const complianceStatements = dep?.kitModule?.compliance - ?.map((x) => { - const control = this.controls.tryFindById(x.control); - if (!control) { - this.logger.warn( - `could not find compliance control ${x.control} referenced in a compliance statement in ${dep.kitModulePath}`, - ); - - return; - } - - return `- [${control.name}](${ - docsRepo.controlLink( - destPath, - x.control, - ) - }): ${x.statement}`; - }) - .filter((x): x is string => !!x); - - const complianceStatementsBlock = !complianceStatements?.length - ? `` - : `## Compliance Statements - -${complianceStatements.join("\n")}`; - return complianceStatementsBlock; - } -} diff --git a/src/docs/PlatformModuleOutputCollector.ts b/src/docs/PlatformModuleOutputCollector.ts deleted file mode 100644 index c46b41a1..00000000 --- a/src/docs/PlatformModuleOutputCollector.ts +++ /dev/null @@ -1,61 +0,0 @@ -import * as path from "std/path"; -import { TerragruntCliFacade } from "../api/terragrunt/TerragruntCliFacade.ts"; -import { KitModuleDependency } from "../kit/KitDependencyAnalyzer.ts"; -import { CollieRepository } from "../model/CollieRepository.ts"; -import { Logger } from "../cli/Logger.ts"; -import { MeshError } from "../errors.ts"; - -/** - * Note: - * For a great UX/DX it's important that running "collie foundation docs" is fast. - * - * We have therefore tried speeding it up by collecting output from platform modules in parallel. - * Unfortunately, it appears that terragrunt does not offer us a good way to reliably get all the outputs from all - * platform modules, see https://github.com/meshcloud/collie-cli/issues/267 - * - * This "fast mode" detection also caused other bugs like https://github.com/meshcloud/collie-cli/issues/269 - * - * In the future, we should maybe investigate cachingas an alternative to parallelization, because usually an engineer - * would re-run "collie foundation docs" only after changing a specific platform module - */ - -export interface PlatformModuleOutputCollector { - getOutput(dep: KitModuleDependency): Promise; -} - -/** - * Collects platform module output by running each platform module individually in series. - */ -export class RunIndividualPlatformModuleOutputCollector - implements PlatformModuleOutputCollector { - constructor( - private readonly repo: CollieRepository, - private readonly terragrunt: TerragruntCliFacade, - private readonly logger: Logger, - ) {} - - async getOutput(dep: KitModuleDependency): Promise { - const result = await this.terragrunt.collectOutput( - this.repo.resolvePath(path.dirname(dep.sourcePath)), - "documentation_md", - ); - - if (!result.status.success) { - this.logger.error( - (fmt) => - `Failed to collect output "documentation_md" from platform module ${ - fmt.kitPath( - dep.sourcePath, - ) - }`, - ); - this.logger.error(result.stderr); - - throw new MeshError( - "Failed to collect documentation output from platform modules", - ); - } - - return result.stdout; - } -} diff --git a/src/foundation/FoundationDeployer.ts b/src/foundation/FoundationDeployer.ts new file mode 100644 index 00000000..0bc5eed6 --- /dev/null +++ b/src/foundation/FoundationDeployer.ts @@ -0,0 +1,109 @@ +import * as path from "std/path"; + +import { + TerragruntArguments, + TerragruntCliFacade, + toVerb, +} from "/api/terragrunt/TerragruntCliFacade.ts"; +import { FoundationRepository } from "/model/FoundationRepository.ts"; +import { Logger } from "../cli/Logger.ts"; +import { ProgressReporter } from "/cli/ProgressReporter.ts"; +import { + CollieRepository, + PLATFORM_MODULE_GLOB, +} from "/model/CollieRepository.ts"; + +export class FoundationDeployer { + constructor( + private readonly repo: CollieRepository, + protected readonly foundation: FoundationRepository, + private readonly terragrunt: TerragruntCliFacade, + private readonly logger: Logger, + ) {} + + async deployFoundationModules( + mode: TerragruntArguments, + module: string | undefined, + autoApprove: boolean, + ) { + const foundationOrModulePath = this.foundation.resolvePath( + module || "", + ); + + const relativePlatformOrModulePath = this.repo.relativePath( + foundationOrModulePath, + ); + + const progress = this.buildProgressReporter( + mode, + relativePlatformOrModulePath, + ); + + const tgfiles = await this.foundationModuleTerragruntFiles( + relativePlatformOrModulePath, + ); + if (tgfiles.length === 0) { + this.logger.warn( + (fmt) => + `detected no foundation modules at ${ + fmt.kitPath(foundationOrModulePath) + }, will skip invoking "terragrunt "`, + ); + + this.logger.tipCommand( + "Apply a kit module to this foundation to create a foundation module using", + "kit apply", + ); + } else if (tgfiles.length === 1) { + // we can't run terragrunt in the platform dir, so we have to infer the platformModule path from + // the discovered terragrunt file + const singleModulePath = path.dirname(tgfiles[0].path); + + this.logger.debug( + (fmt) => + `detected a single foundation module at ${ + fmt.kitPath(singleModulePath) + }, will deploy with "terragrunt "`, + ); + await this.terragrunt.run(singleModulePath, mode, { autoApprove }); + } else { + this.logger.debug( + (fmt) => + `detected a stack of foundation modules at ${ + fmt.kitPath( + foundationOrModulePath, + ) + }, will deploy with "terragrunt run-all "`, + ); + + await this.terragrunt.runAll(foundationOrModulePath, mode, { + excludeDirs: [PLATFORM_MODULE_GLOB], // if we let terragrunt run a run-all, need to explicitly exclude all platform modules + autoApprove, + }); + } + + progress.done(); + } + + private async foundationModuleTerragruntFiles(relativeModulePath: string) { + const excludes = { + testModules: true, + tenantModules: true, + platformModules: true, + }; + + const files = await this.repo.processFilesGlob( + // todo: exclude platforms/folder + `${relativeModulePath}/**/terragrunt.hcl`, + (file) => file, + excludes, + ); + + // a terragrunt stack conists of multiple executable terragrunt files + return files; + } + + private buildProgressReporter(mode: TerragruntArguments, id: string) { + return new ProgressReporter(toVerb(mode), id, this.logger); + } +} diff --git a/src/foundation/PlatformDeployer.ts b/src/foundation/PlatformDeployer.ts index 71110c16..f7dd396d 100644 --- a/src/foundation/PlatformDeployer.ts +++ b/src/foundation/PlatformDeployer.ts @@ -173,6 +173,7 @@ export class PlatformDeployer { const excludes = { testModules: false, tenantModules: true, + platformModules: false, }; const files = await this.repo.processFilesGlob( diff --git a/src/kit/KitDependencyAnalyzer.ts b/src/kit/KitDependencyAnalyzer.ts index 9f478388..4eb07a69 100644 --- a/src/kit/KitDependencyAnalyzer.ts +++ b/src/kit/KitDependencyAnalyzer.ts @@ -5,6 +5,7 @@ import { KitModule } from "./KitModule.ts"; import { Logger } from "../cli/Logger.ts"; import { ProgressReporter } from "../cli/ProgressReporter.ts"; import { CollieRepository } from "../model/CollieRepository.ts"; +import { ComplianceControlRepository } from "../compliance/ComplianceControlRepository.ts"; export interface FoundationDependencies { foundation: string; @@ -42,6 +43,7 @@ export class KitDependencyAnalyzer { constructor( private readonly collie: CollieRepository, private readonly kitModules: KitModuleRepository, + private readonly complianceControls: ComplianceControlRepository, private readonly logger: Logger, ) {} @@ -64,6 +66,7 @@ export class KitDependencyAnalyzer { const excludes = { tenantModules: true, testModules: true, + platformModules: false, }; const q = await this.collie.processFilesGlob( `${relativePlatformPath}/**/terragrunt.hcl`, @@ -111,7 +114,10 @@ export class KitDependencyAnalyzer { const msg = `Could not find kit module with id ${kitModuleId} included from ${sourcePath}`; this.logger.warn(msg); + } else { + this.validateKitModuleComplianceStatements(kitModule, kitModulePath); } + return { sourcePath, kitModuleId, @@ -119,6 +125,18 @@ export class KitDependencyAnalyzer { kitModule, }; } + validateKitModuleComplianceStatements( + kitModule: KitModule, + kitModulePath: string, + ) { + (kitModule?.compliance || []).forEach((x) => { + if (!this.complianceControls.tryFindById(x.control)) { + this.logger.warn( + `Could not find compliance control ${x.control} referenced in a compliance statement in ${kitModulePath}`, + ); + } + }); + } static parseTerraformSource(hcl: string): string | undefined { const regex = /terraform {[\s\S]+source = "([\s\S]+?)"/g; diff --git a/src/model/CollieHub.ts b/src/model/CollieHub.ts index a3badb1d..13264545 100644 --- a/src/model/CollieHub.ts +++ b/src/model/CollieHub.ts @@ -3,6 +3,7 @@ import { GitCliFacade } from "/api/git/GitCliFacade.ts"; import { CollieRepository } from "/model/CollieRepository.ts"; import { CollieConfig } from "./CollieConfig.ts"; import { Logger } from "../cli/Logger.ts"; +import { rimraf } from "../path.ts"; export class CollieHub { constructor( @@ -82,6 +83,6 @@ export class CollieHub { public async cleanHubClone() { const hubCacheDir = this.repo.resolvePath(...this.hubCacheDirPath); - await Deno.remove(hubCacheDir, { recursive: true }); + await rimraf(hubCacheDir); } } diff --git a/src/model/CollieRepository.ts b/src/model/CollieRepository.ts index 79f16dd0..3b57f1d0 100644 --- a/src/model/CollieRepository.ts +++ b/src/model/CollieRepository.ts @@ -3,7 +3,7 @@ import * as path from "std/path"; export const TEST_MODULE_GLOB = "**/*.test"; export const TENANT_MODULE_GLOB = "**/tenants"; - +export const PLATFORM_MODULE_GLOB = "**/platforms"; export class CollieRepository { private constructor(private readonly repoDir: string) { if (!path.isAbsolute(repoDir)) { @@ -56,9 +56,11 @@ export class CollieRepository { excludes: { testModules: boolean; tenantModules: boolean; + platformModules: boolean; } = { testModules: true, tenantModules: false, + platformModules: false, }, ): Promise { const q: T[] = []; @@ -79,6 +81,7 @@ export class CollieRepository { ...(excludes.testModules ? [TEST_MODULE_GLOB] : []), ...(excludes.tenantModules ? [TENANT_MODULE_GLOB] : []), + ...(excludes.platformModules ? [PLATFORM_MODULE_GLOB] : []), ], }) ) { diff --git a/src/path.ts b/src/path.ts index fe674f12..8b86631e 100644 --- a/src/path.ts +++ b/src/path.ts @@ -10,3 +10,21 @@ import * as posix from "std/path/posix"; export function convertToPosixPath(relativePath: string) { return relativePath.split(path.SEPARATOR).join(posix.SEPARATOR); } + +/** + * Removes a dir, including all its contents. Does not throw if the dir does not exist. + * Equivalent to `rm -rf dir` + * + * @param dir the directory to remove + */ +export async function rimraf(dir: string) { + try { + await Deno.remove(dir, { recursive: true }); + } catch (error) { + if (error instanceof Deno.errors.NotFound) { + // ignore, dir is already removed + } else { + throw error; + } + } +}