diff --git a/default.nix b/default.nix index a1c68417..e026edac 100644 --- a/default.nix +++ b/default.nix @@ -18,5 +18,13 @@ pkgs.mkShell { pkgs.azure-cli pkgs.google-cloud-sdk + # terraform + pkgs.terraform + pkgs.terragrunt + pkgs.tflint + pkgs.terraform-docs + + # for collie foundation docs + pkgs.nodejs ]; } diff --git a/src/commands/foundation/docs.command.ts b/src/commands/foundation/docs.command.ts index 0d8457b0..d8cb576b 100644 --- a/src/commands/foundation/docs.command.ts +++ b/src/commands/foundation/docs.command.ts @@ -1,3 +1,4 @@ +import * as fs from "std/fs"; import { CliApiFacadeFactory } from "../../api/CliApiFacadeFactory.ts"; import { DirectoryGenerator, WriteMode } from "../../cli/DirectoryGenerator.ts"; import { Logger } from "../../cli/Logger.ts"; @@ -8,7 +9,6 @@ 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 { VuepressDocumentationSiteGenerator } from "../../docs/VuepressDocumentationSiteGenerator.ts"; import { KitDependencyAnalyzer } from "../../kit/KitDependencyAnalyzer.ts"; import { KitModuleRepository } from "../../kit/KitModuleRepository.ts"; import { CollieRepository } from "../../model/CollieRepository.ts"; @@ -46,7 +46,7 @@ export function registerDocsCmd(program: TopLevelCommand) { const factory = new CliApiFacadeFactory(repo, logger); // todo: instead of flags, maybe these should be subcommands? if (opts.update) { - await updateDocumentation(repo, foundationRepo, factory, logger); + await updateDocumentation(repo, foundationRepo, logger); } if (opts.preview) { @@ -69,7 +69,6 @@ export function registerDocsCmd(program: TopLevelCommand) { async function updateDocumentation( repo: CollieRepository, foundation: FoundationRepository, - factory: CliApiFacadeFactory, logger: Logger, ) { const foundationProgress = new ProgressReporter( @@ -79,9 +78,7 @@ async function updateDocumentation( ); const dir = new DirectoryGenerator(WriteMode.overwrite, logger); - const siteGenerator = new VuepressDocumentationSiteGenerator(dir, foundation); - const tfDocs = factory.buildTerraformDocs(); const validator = new ModelValidator(logger); const modules = await KitModuleRepository.load(repo, validator, logger); const controls = await ComplianceControlRepository.load( @@ -93,7 +90,6 @@ async function updateDocumentation( repo, modules, controls, - tfDocs, logger, ); @@ -114,8 +110,9 @@ async function updateDocumentation( const docsRepo = new DocumentationRepository(foundation); + await prepareSiteTemplate(docsRepo, repo, logger); + const generator = new DocumentationGenerator( - siteGenerator, moduleDocumentation, complianceDocumentation, platformDocumentation, @@ -131,10 +128,41 @@ async function previewDocumentation( factory: CliApiFacadeFactory, ) { const docsRepo = new DocumentationRepository(foundation); - const dir = foundation.resolvePath(docsRepo.docsRootDir); + const dir = docsRepo.docsRootPath; const npm = factory.buildNpm(); await npm.run(["install"], { cwd: dir }); await npm.run(["run", "docs:dev"], { 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.docsRootPath, { 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/kit/compile.command.ts b/src/commands/kit/compile.command.ts new file mode 100644 index 00000000..6482c05c --- /dev/null +++ b/src/commands/kit/compile.command.ts @@ -0,0 +1,42 @@ +import { CollieRepository } from "../../model/CollieRepository.ts"; +import { GlobalCommandOptions } from "../GlobalCommandOptions.ts"; +import { Logger } from "../../cli/Logger.ts"; +import { ModelValidator } from "../../model/schemas/ModelValidator.ts"; +import { KitModuleRepository } from "../../kit/KitModuleRepository.ts"; +import { TopLevelCommand } from "../TopLevelCommand.ts"; +import { CliApiFacadeFactory } from "../../api/CliApiFacadeFactory.ts"; +import { ProgressReporter } from "../../cli/ProgressReporter.ts"; + +export function registerCompileCmd(program: TopLevelCommand) { + program + .command("compile [module]") + .description("Compile kit modules, updating their documentation") + .action(async (opts: GlobalCommandOptions, module?: string) => { + const collie = new CollieRepository("./"); + const logger = new Logger(collie, opts); + const validator = new ModelValidator(logger); + const moduleRepo = await KitModuleRepository.load( + collie, + validator, + logger, + ); + + const progress = new ProgressReporter( + "compiling", + "kit modules", + logger, + ); + + // todo: should compiling a kit module also run tflint and other stuff? + const factory = new CliApiFacadeFactory(collie, logger); + const tfDocs = factory.buildTerraformDocs(); + + const tasks = moduleRepo.all + .filter((x) => !module || module == x.id) + .map(async (x) => await tfDocs.updateReadme(x.kitModulePath)); + + await Promise.all(tasks); + + progress.done(); + }); +} diff --git a/src/commands/kit/kit.command.ts b/src/commands/kit/kit.command.ts index f309ac08..9438f765 100644 --- a/src/commands/kit/kit.command.ts +++ b/src/commands/kit/kit.command.ts @@ -1,6 +1,7 @@ import { makeTopLevelCommand, TopLevelCommand } from "../TopLevelCommand.ts"; import { registerApplyCmd } from "./apply.command.ts"; import { registerBundledKitCmd } from "./bundle.command.ts"; +import { registerCompileCmd } from "./compile.command.ts"; import { registerImportCmd } from "./import.command.ts"; import { registerNewCmd } from "./new.command.ts"; import { registerTreeCmd } from "./tree.command.ts"; @@ -8,10 +9,11 @@ import { registerTreeCmd } from "./tree.command.ts"; export function registerKitCommand(program: TopLevelCommand) { const kitCommands = makeTopLevelCommand(); registerNewCmd(kitCommands); - registerBundledKitCmd(kitCommands); - registerImportCmd(kitCommands); registerApplyCmd(kitCommands); + registerImportCmd(kitCommands); registerTreeCmd(kitCommands); + registerBundledKitCmd(kitCommands); + registerCompileCmd(kitCommands); program .command("kit", kitCommands) diff --git a/src/docs/DocumentationGenerator.ts b/src/docs/DocumentationGenerator.ts index 896309e3..04e1b411 100644 --- a/src/docs/DocumentationGenerator.ts +++ b/src/docs/DocumentationGenerator.ts @@ -2,19 +2,15 @@ import { ComplianceDocumentationGenerator } from "./ComplianceDocumentationGener import { DocumentationRepository } from "./DocumentationRepository.ts"; import { KitModuleDocumentationGenerator } from "./KitModuleDocumentationGenerator.ts"; import { PlatformDocumentationGenerator } from "./PlatformDocumentationGenerator.ts"; -import { VuepressDocumentationSiteGenerator } from "./VuepressDocumentationSiteGenerator.ts"; export class DocumentationGenerator { constructor( - private readonly siteGenerator: VuepressDocumentationSiteGenerator, private readonly kitModuleDocumentation: KitModuleDocumentationGenerator, private readonly complianceDocumentation: ComplianceDocumentationGenerator, private readonly platformDocumentation: PlatformDocumentationGenerator, ) {} async generateFoundationDocumentation(docsRepo: DocumentationRepository) { - await this.siteGenerator.generateSite(docsRepo); - // todo: can we flatten the duplicate docs/ folder nesting? await this.complianceDocumentation.generate(docsRepo); await this.kitModuleDocumentation.generate(docsRepo); diff --git a/src/docs/DocumentationRepository.ts b/src/docs/DocumentationRepository.ts index 38c10bef..14e517a5 100644 --- a/src/docs/DocumentationRepository.ts +++ b/src/docs/DocumentationRepository.ts @@ -5,20 +5,23 @@ 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. - public readonly docsRootDir = ".docs"; - public readonly docsContentDir = "docs"; + private readonly docsRootDir = ".docs"; + private readonly docsContentDir = "docs"; // these paths are the same in collie repository and docs content public readonly platformsDir = "platforms"; public readonly complianceDir = "compliance"; public readonly kitDir = "kit"; + public readonly docsRootPath: string; public readonly docsContentPath: string; public readonly kitPath: string; public readonly compliancePath: string; public readonly platformsPath: string; constructor(foundation: FoundationRepository) { + this.docsRootPath = foundation.resolvePath(this.docsRootDir); + this.docsContentPath = foundation.resolvePath( this.docsRootDir, this.docsContentDir, diff --git a/src/docs/KitModuleDocumentationGenerator.ts b/src/docs/KitModuleDocumentationGenerator.ts index 43cafd6f..93ca23bd 100644 --- a/src/docs/KitModuleDocumentationGenerator.ts +++ b/src/docs/KitModuleDocumentationGenerator.ts @@ -1,6 +1,5 @@ import * as fs from "std/fs"; import * as path from "std/path"; -import { TerraformDocsCliFacade } from "../api/terraform-docs/TerraformDocsCliFacade.ts"; import { Logger } from "../cli/Logger.ts"; import { ProgressReporter } from "../cli/ProgressReporter.ts"; @@ -15,7 +14,6 @@ export class KitModuleDocumentationGenerator { private readonly collie: CollieRepository, private readonly kitModules: KitModuleRepository, private readonly controls: ComplianceControlRepository, - private readonly tfdocs: TerraformDocsCliFacade, private readonly logger: Logger, ) {} @@ -34,7 +32,7 @@ export class KitModuleDocumentationGenerator { this.logger.verbose((fmt) => `generating ${fmt.kitPath(dest)}`); - const md = await this.generateModuleDocumentation(x, docsRepo); + const md = this.generateModuleDocumentation(x, docsRepo); await Deno.mkdir(path.dirname(dest), { recursive: true }); await Deno.writeTextFile(dest, md); @@ -57,12 +55,10 @@ export class KitModuleDocumentationGenerator { await fs.copy(source, dest, { overwrite: true }); } - private async generateModuleDocumentation( + private generateModuleDocumentation( parsed: ParsedKitModule, docsRepo: DocumentationRepository, ) { - await this.tfdocs.updateReadme(parsed.kitModulePath); - const complianceStatements = this.generateComplianceStatements( parsed, docsRepo, diff --git a/src/docs/VuepressDocumentationSiteGenerator.ts b/src/docs/VuepressDocumentationSiteGenerator.ts deleted file mode 100644 index 61bae8c5..00000000 --- a/src/docs/VuepressDocumentationSiteGenerator.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { Dir, DirectoryGenerator } from "../cli/DirectoryGenerator.ts"; -import { FoundationRepository } from "../model/FoundationRepository.ts"; -import { DocumentationRepository } from "./DocumentationRepository.ts"; - -/** - * Generates a skeleton for a Vuepress documentation site. - * - * In the future we could support other kinds of documentation site generators. - * In general collie's primary job is to output good markdown, we only include vuepress - * site generator for convenience/demo. - */ -export class VuepressDocumentationSiteGenerator { - constructor( - private readonly dir: DirectoryGenerator, - private readonly foundation: FoundationRepository, - ) {} - - async generateSite(docsRepo: DocumentationRepository) { - const d: Dir = { - name: docsRepo.docsRootDir, - entries: [ - { name: "package.json", content: this.generatePackageJson() }, - { name: ".gitignore", content: this.generateGitignore() }, - { - name: docsRepo.docsContentDir, - entries: [ - { - name: ".vuepress", - entries: [ - { - name: "config.ts", - content: this.generateVuepressConfig(), - }, - ], - }, - ], - }, - ], - }; - - await this.dir.write(d, this.foundation.resolvePath()); - } - - private generateVuepressConfig() { - return `import * as fs from "fs"; -import * as path from "path"; -import { - DefaultThemeOptions, - defineUserConfig, - ViteBundlerOptions, -} from "vuepress-vite"; -import type { SidebarConfig, NavbarConfig } from "@vuepress/theme-default"; - -const navbar: NavbarConfig = [ - { text: "Foundation", link: "/" }, - { - text: "Platforms", - link: "/platforms/", - }, - { - text: "Kit", - link: "/kit/", - }, - { - text: "Compliance", - link: "/compliance/", - }, -]; - -function getMarkdownFiles(dir: string) { - const entries = fs.readdirSync(dir, { withFileTypes: true }); - - const mdFiles = entries - .filter( - (x) => x.isFile() && !x.name.startsWith(".") && x.name.endsWith(".md") - ) - .map((x) => "/" + path.relative("docs/", path.join(dir, x.name))) - .map((x) => x.replaceAll(path.sep, '/')); //on windows, this needs to be done to cleanly define URL paths - - return mdFiles; -} - -function getTree(dir: string) { - return fs - .readdirSync(dir, { withFileTypes: true }) - .filter((x) => x.isDirectory()) - .map((x) => { - const child = path.join(dir, x.name); - return { - text: x.name, - collapsible: true, - children: [ - ...getMarkdownFiles(child), - ...getTree(child), - ], - }; - }); -} - -export const sidebar: SidebarConfig = { - "/platforms/": [ - { - text: "Platforms", - children: getMarkdownFiles("docs/platforms"), - }, - ], - "/kit/": [ - { - text: "Kit", - children: [...getMarkdownFiles("docs/kit")], - }, - ...getTree("docs/kit"), - ], - "/compliance/": [ - { - text: "Compliance", - children: [...getMarkdownFiles("docs/compliance")], - }, - ...getTree("docs/compliance"), - ], -}; - -export default defineUserConfig({ - // site-level locales config - locales: { - "/": { - lang: "en-US", - title: "${this.foundation.name} Cloud Foundation", - description: "Documentation for the ${this.foundation.name} cloud foundations", - }, - }, - - themeConfig: { - locales: { - "/": { - navbar: navbar, - sidebar: sidebar, - }, - }, - }, - plugins: [ - [ - "@vuepress/plugin-git", - { - createdTime: false, - updateTime: true, - contributors: false, - }, - ], - ], -}); -`; - } - - private generatePackageJson() { - const config = { - name: this.foundation.id + "-docs", - version: "1.0.0", - private: "true", - scripts: { - "docs:dev": "vuepress dev docs", - "docs:build": "vuepress build docs", - }, - devDependencies: { - vuepress: "2.0.0-beta.37", - "@vuepress/plugin-search": "2.0.0-beta.37", - }, - }; - - return JSON.stringify(config, null, 2); - } - private generateGitignore(): string { - return `node_modules -.temp -.cache -`; - } -}