// SPDX-FileCopyrightText: the secureCodeBox authors // // SPDX-License-Identifier: Apache-2.0 const fs = require("fs"), rimraf = require("rimraf"), colors = require("colors"), matter = require("gray-matter"), path = require('path'), { docsConfig: config } = require("./utils/config"), { removeWhitespaces } = require("./utils/capitalizer"), Mustache = require("mustache"); colors.setTheme({ info: "blue", help: "cyan", warn: "yellow", success: "green", error: "red", }); // For the documentation on this script look at the README.md of this repository async function main() { const currentDirectory = __dirname; // current directory is /documentation/src const parentDirectory = path.dirname(currentDirectory); // parent is /documentation const rootDirectory = path.dirname(parentDirectory); // root is / const dataArray = await Promise.all( config.filesFromRepository.map((dir) => readDirectory(`${rootDirectory}/${dir.src}`, false) .then((res) => ({ ...dir, files: res })) .catch((err) => console.error( `ERROR: Could not read directory at: ${dir}`.error, err.message.error ) ) ) ); if (!fs.existsSync(config.targetPath)) { fs.mkdirSync(config.targetPath); } // Clear preexisting findings if (fs.existsSync(config.findingsDir)) { rimraf.sync(config.findingsDir); } for (const dir of dataArray) { const trgDir = `${config.targetPath}/${dir.dst}`; const srcDir = `${rootDirectory}/${dir.src}`; // Clears existing md files from directories if (fs.existsSync(trgDir)) { await removeExistingMarkdownFilesFromDirectory(trgDir, dir.keep); console.warn( `WARN: ${trgDir.info} already existed and was overwritten.`.warn ); } else { fs.mkdirSync(trgDir); } // If the source directory contains a ".helm-docs.gotmpl" file (such as in /scanners or /hooks), the doc files need to be generated. // Else, the docs files are just copied to the destination path. dir.files.includes(".helm-docs.gotmpl") ? await createDocFilesFromMainRepository(srcDir, trgDir, await readDirectory(srcDir)) : await copyFilesFromMainRepository(srcDir, trgDir, dir.exclude, dir.keep); } } main().catch((err) => { clearDocsOnFailure(); console.error(err.stack.error); }); function readDirectory(dir, dirNamesOnly = true) { return new Promise((res, rej) => { fs.readdir( dir, { encoding: "utf8", withFileTypes: true }, function (err, data) { if (err) { rej(err); } else { if (dirNamesOnly) data = data.filter((file) => file.isDirectory()); const directories = data.map((dirent) => dirent.name); res(directories); } } ); }); } async function createDocFilesFromMainRepository(relPath, targetPath, dirNames) { for (const dirName of dirNames) { const readMe = `${relPath}/${dirName}/README.md`; if (!fs.existsSync(readMe)) { console.log( `WARN: Skipping ${dirName.help}: file not found at ${readMe.info}.`.warn ); continue; } // Read readme content of scanner / hook directory const readmeContent = fs.readFileSync(readMe, { encoding: "utf8" }); const examples = await getExamples(`${relPath}/${dirName}/examples`); const imageTypes = await getSupportedImageTypes(`${relPath}/${dirName}/Chart.yaml`); // Add a custom editUrl to the frontMatter to ensure that it points to the correct repo const { data: frontmatter, content } = matter(readmeContent); // Either the path contains "secureCodeBox" or "repo" depending on whether the docs are locally generated or in netlify const filePathInRepo = relPath.replace(/^.*(?:secureCodeBox|repo)\//, ""); const readmeWithEditUrl = matter.stringify(content, { ...frontmatter, description: frontmatter?.usecase, custom_edit_url: `https://github.com/${config.repository}/edit/${config.branch}/${filePathInRepo}/${dirName}/.helm-docs.gotmpl`, }); // Skip File if its marked as "hidden" in its frontmatter if (frontmatter.hidden !== undefined && frontmatter.hidden === true) { continue; } const integrationPage = Mustache.render( fs.readFileSync(path.join(__dirname, "utils/scannerReadme.mustache"), { encoding: "utf8", }), { readme: readmeWithEditUrl, examples, hasExamples: examples.length !== 0, imageTypes, hasImageTypes: imageTypes?.length > 0 } ); let fileName = frontmatter.title ? frontmatter.title : dirName; //Replace Spaces in the FileName with "-" and convert to lower case to avoid URL issues fileName = fileName.replace(/ /g, "-").toLowerCase(); const filePath = `${targetPath}/${fileName}.md`; fs.writeFileSync(filePath, integrationPage); console.log( `SUCCESS: Created file for ${dirName.help} at ${filePath.info}`.success ); } } async function getExamples(dir) { if (!fs.existsSync(dir)) { return []; } const dirNames = await readDirectory(dir).catch(() => []); if (dirNames.length === 0) { console.warn(`WARN: Found empty examples folder at ${dir.info}.`.warn); return []; } return dirNames.map((dirName) => { let readMe = ""; if (fs.existsSync(`${dir}/${dirName}/README.md`)) { readMe = matter( fs.readFileSync(`${dir}/${dirName}/README.md`, { encoding: "utf8", }) ).content; } let scanContent = null; if (fs.existsSync(`${dir}/${dirName}/scan.yaml`)) { scanContent = fs.readFileSync(`${dir}/${dirName}/scan.yaml`, { encoding: "utf8", }); } let findingContent = null; let findingSizeLimitReached = null; if (fs.existsSync(`${dir}/${dirName}/findings.yaml`)) { findingSizeLimitReached = fs.statSync(`${dir}/${dirName}/findings.yaml`).size >= config.sizeLimit; if (findingSizeLimitReached) { console.warn( `WARN: Findings for ${dirName.info} exceeded size limit.`.warn ); findingContent = copyFindingsForDownload( `${dir}/${dirName}/findings.yaml` ); } else { findingContent = fs.readFileSync(`${dir}/${dirName}/findings.yaml`, { encoding: "utf8", }); } } let findings = null; if (findingContent && findingSizeLimitReached !== null) { findings = { value: findingContent, limitReached: findingSizeLimitReached, }; } return { name: removeWhitespaces(dirName), exampleReadme: readMe, scan: scanContent, findings, }; }); } function getSupportedImageTypes(dir) { if (fs.existsSync(dir)) { const chartContent = fs.readFileSync(dir, { encoding: "utf8", }); // add an opening delimiter to help matter distinguish the file type const { data: frontmatter} = matter(['---', ...chartContent.toString().split('\n')].join('\n')); if ('annotations' in frontmatter && 'supported-platforms' in frontmatter.annotations) { return frontmatter['annotations']['supported-platforms'].split(','); } } } function copyFindingsForDownload(filePath) { const dirNames = filePath.split("/"), name = dirNames[dirNames.indexOf("examples") - 1] + "-" + dirNames[dirNames.indexOf("examples") + 1], targetPath = `/${config.findingsDir}/${name}-findings.yaml`; if (!fs.existsSync("static")) { fs.mkdirSync("static/"); } if (!fs.existsSync(`static/${config.findingsDir}`)) { fs.mkdirSync(`static/${config.findingsDir}`); } fs.copyFileSync(filePath, "static" + targetPath); console.log(`SUCCESS: Created download link for ${name.info}.`.success); return targetPath; } function clearDocsOnFailure() { for (const dir of config.filesFromRepository) { const trgDir = `${config.targetPath}/${dir.src}`; if (fs.existsSync(trgDir)) { removeExistingMarkdownFilesFromDirectory(trgDir, dir.keep) .then(() => { console.log( `Cleared ${trgDir.info} due to previous failure.`.magenta ); }) .catch((err) => { console.error( `ERROR: Could not remove ${trgDir.info} on failure.`.error ); console.error(err.message.error); }); } } } // Copy files from a given src directory from the main repo into the given dst directory // // Example: copyFilesFromMainRepository("docs/adr", "docs/architecture/adr"); // copyFilesFromMainRepository("docs/adr", "docs/architecture/adr", ["adr_0000.md", "adr_README.md"]); // // @param src required source directory in main repository (docsConfig.repository) // @param dst required target directory in this repository relative to config.targetPath // @param exclude optional array of files to exclude from srcPath // @param keep optional array of files to keep in dstPath async function copyFilesFromMainRepository(srcPath, dstPath, exclude, keep) { exclude = exclude || []; keep = keep || []; if (fs.existsSync(srcPath)) { console.error(`${srcPath.info}.`.error); } if (fs.existsSync(dstPath)) { await removeExistingMarkdownFilesFromDirectory(dstPath, keep); } else { fs.mkdirSync(dstPath); console.info(`Create target directory ${dstPath.info}...`.success); } fs.readdirSync(srcPath).map((fileName) => { if (!exclude.includes(fileName)) { console.log(`Copy ${fileName.info} to ${dstPath.info}...`.success); fs.copyFileSync(`${srcPath}/${fileName}`, `${dstPath}/${fileName}`); } }); } async function removeExistingMarkdownFilesFromDirectory(dirPath, filesToKeep) { console.info(`Remove existing markdown files from ${dirPath.info}`) const allFiles = await readDirectory(dirPath, false); allFiles .filter((fileName) => fileName.endsWith(".md")) .filter(fileName => doNotKeepFile(fileName, filesToKeep)) .forEach((fileName) => { const filePath = `${dirPath}/${fileName}`; rimraf.sync(filePath); console.warn(`WARN: ${filePath} was deleted.`.warn); }); } function doNotKeepFile(fileName, filesToKeep) { // Helper method to make it harder to oversee the !. It is easier to see the negation in the name instead // somewhere in the used filter invocation. return !keepFile(fileName, filesToKeep); } function keepFile(fileName, filesToKeep) { console.info(`Determine whether to keep '${fileName}' (${filesToKeep})`.info); for (let index in filesToKeep) { const fileToKeep = filesToKeep[index]; if (fileName.normalize() === fileToKeep.normalize()) { console.info(`Keeping file ${fileName}`.info); return true; } } return false; }