UNPKG

@redpanda-data/docs-extensions-and-macros

Version:

Antora extensions and macros developed for Redpanda documentation.

445 lines (381 loc) 18.2 kB
'use strict'; const fs = require('fs'); const path = require('path'); const handlebars = require('handlebars'); const helpers = require('./helpers'); /** * Handlebars documentation generator for Redpanda configuration properties. * * Supports custom template overrides using environment variables: * - TEMPLATE_PROPERTY_PAGE: Main property page template * - TEMPLATE_PROPERTY: Individual property section template * - TEMPLATE_TOPIC_PROPERTY: Individual topic property section template * - TEMPLATE_DEPRECATED_PROPERTY: Individual deprecated property section template * - TEMPLATE_DEPRECATED: Deprecated properties page template * * CLI Usage: node generate-handlebars-docs.js <input-file> <output-dir> */ // Register all helpers Object.entries(helpers).forEach(([name, fn]) => { if (typeof fn !== 'function') { console.error(`❌ Helper "${name}" is not a function`); process.exit(1); } handlebars.registerHelper(name, fn); }); /** * Configuration mapping for different property types */ const PROPERTY_CONFIG = { broker: { pageTitle: 'Broker Configuration Properties', pageAliases: ['reference:node-properties.adoc', 'reference:node-configuration-sample.adoc'], description: 'Reference of broker configuration properties.', intro: `Broker configuration properties are applied individually to each broker in a cluster. You can find and modify these properties in the \`redpanda.yaml\` configuration file. For information on how to edit broker properties, see xref:manage:cluster-maintenance/node-property-configuration.adoc[]. NOTE: All broker properties require that you restart Redpanda for any update to take effect.`, sectionTitle: 'Broker configuration', groups: [ { filter: (prop) => prop.config_scope === 'broker' && !prop.is_deprecated } ], filename: 'broker-properties.adoc' }, cluster: { pageTitle: 'Cluster Configuration Properties', pageAliases: ['reference:tunable-properties.adoc', 'reference:cluster-properties.adoc'], description: 'Cluster configuration properties list.', intro: `Cluster configuration properties are the same for all brokers in a cluster, and are set at the cluster level. For information on how to edit cluster properties, see xref:manage:cluster-maintenance/cluster-property-configuration.adoc[] or xref:manage:kubernetes/k-cluster-property-configuration.adoc[]. NOTE: Some cluster properties require that you restart the cluster for any updates to take effect. See the specific property details to identify whether or not a restart is required.`, sectionTitle: 'Cluster configuration', groups: [ { filter: (prop) => prop.config_scope === 'cluster' && !prop.is_deprecated } ], filename: 'cluster-properties.adoc' }, 'object-storage': { pageTitle: 'Object Storage Properties', description: 'Reference of object storage properties.', intro: `Object storage properties are a type of cluster property. For information on how to edit cluster properties, see xref:manage:cluster-maintenance/cluster-property-configuration.adoc[]. NOTE: Some object storage properties require that you restart the cluster for any updates to take effect. See the specific property details to identify whether or not a restart is required.`, sectionTitle: 'Object storage configuration', sectionIntro: 'Object storage properties should only be set if you enable xref:manage:tiered-storage.adoc[Tiered Storage].', groups: [ { filter: (prop) => prop.name && ( prop.name.includes('cloud_storage') || prop.name.includes('s3_') || prop.name.includes('azure_') || prop.name.includes('gcs_') || prop.name.includes('archival_') || prop.name.includes('remote_') || prop.name.includes('tiered_') ) && !prop.is_deprecated } ], filename: 'object-storage-properties.adoc' }, topic: { pageTitle: 'Topic Configuration Properties', pageAliases: ['reference:topic-properties.adoc'], description: 'Reference of topic configuration properties.', intro: `A topic-level property sets a Redpanda or Kafka configuration for a particular topic. Many topic-level properties have corresponding xref:manage:cluster-maintenance/cluster-property-configuration.adoc[cluster properties] that set a default value for all topics of a cluster. To customize the value for a topic, you can set a topic-level property that overrides the value of the corresponding cluster property. NOTE: All topic properties take effect immediately after being set.`, sectionTitle: 'Topic configuration', groups: [ { filter: (prop) => prop.config_scope === 'topic' && !prop.is_deprecated, template: 'topic-property' } ], filename: 'topic-properties.adoc' } }; // "src/v/kafka/server/handlers/topics/types.cc": "topic" /** * Gets template path, checking environment variables for custom paths first */ function getTemplatePath(defaultPath, envVar) { const customPath = process.env[envVar]; if (customPath && fs.existsSync(customPath)) { console.log(`📄 Using custom template: ${customPath}`); return customPath; } return defaultPath; } /** * Register Handlebars partials used to render property documentation. * * Loads templates from the local templates directory (overridable via environment * variables handled by getTemplatePath) and registers three partials: * - "property" (uses cloud-aware `property-cloud.hbs` when enabled) * - "topic-property" (uses cloud-aware `topic-property-cloud.hbs` when enabled) * - "deprecated-property" * * @param {boolean} [hasCloudSupport=false] - If true, select cloud-aware templates for the `property` and `topic-property` partials. * @throws {Error} If any required template file is missing or cannot be read; errors are rethrown after logging. */ function registerPartials(hasCloudSupport = false) { const templatesDir = path.join(__dirname, 'templates'); try { console.log(`📝 Registering Handlebars templates (cloud support: ${hasCloudSupport ? 'enabled' : 'disabled'})`); // Register property partial (choose cloud or regular version) const propertyTemplateFile = hasCloudSupport ? 'property-cloud.hbs' : 'property.hbs'; const propertyTemplatePath = getTemplatePath( path.join(templatesDir, propertyTemplateFile), 'TEMPLATE_PROPERTY' ); if (!fs.existsSync(propertyTemplatePath)) { throw new Error(`Property template not found: ${propertyTemplatePath}`); } const propertyTemplate = fs.readFileSync(propertyTemplatePath, 'utf8'); handlebars.registerPartial('property', propertyTemplate); console.log(`✅ Registered property template: ${propertyTemplateFile}`); // Register topic property partial (choose cloud or regular version) const topicPropertyTemplateFile = hasCloudSupport ? 'topic-property-cloud.hbs' : 'topic-property.hbs'; const topicPropertyTemplatePath = getTemplatePath( path.join(templatesDir, topicPropertyTemplateFile), 'TEMPLATE_TOPIC_PROPERTY' ); if (!fs.existsSync(topicPropertyTemplatePath)) { throw new Error(`Topic property template not found: ${topicPropertyTemplatePath}`); } const topicPropertyTemplate = fs.readFileSync(topicPropertyTemplatePath, 'utf8'); handlebars.registerPartial('topic-property', topicPropertyTemplate); console.log(`✅ Registered topic property template: ${topicPropertyTemplateFile}`); // Register deprecated property partial const deprecatedPropertyTemplatePath = getTemplatePath( path.join(templatesDir, 'deprecated-property.hbs'), 'TEMPLATE_DEPRECATED_PROPERTY' ); if (!fs.existsSync(deprecatedPropertyTemplatePath)) { throw new Error(`Deprecated property template not found: ${deprecatedPropertyTemplatePath}`); } const deprecatedPropertyTemplate = fs.readFileSync(deprecatedPropertyTemplatePath, 'utf8'); handlebars.registerPartial('deprecated-property', deprecatedPropertyTemplate); console.log(`✅ Registered deprecated property template`); } catch (error) { console.error('❌ Failed to register Handlebars templates:'); console.error(` Error: ${error.message}`); console.error(' This indicates missing or corrupted template files.'); console.error(' Check that all .hbs files exist in tools/property-extractor/templates/'); throw error; } } /** * Generates documentation for a specific property type */ function generatePropertyDocs(properties, config, outputDir) { const templatePath = getTemplatePath( path.join(__dirname, 'templates', 'property-page.hbs'), 'TEMPLATE_PROPERTY_PAGE' ); const template = handlebars.compile(fs.readFileSync(templatePath, 'utf8')); // Filter and group properties according to configuration const groups = config.groups.map(group => { const filteredProperties = Object.values(properties) .filter(prop => group.filter(prop)) .sort((a, b) => String(a.name || '').localeCompare(String(b.name || ''))); return { title: group.title, intro: group.intro, properties: filteredProperties, template: group.template || 'property' // Default to 'property' template }; }).filter(group => group.properties.length > 0); const data = { ...config, groups }; const output = template(data); const outputPath = path.join(outputDir, config.filename); fs.mkdirSync(path.dirname(outputPath), { recursive: true }); fs.writeFileSync(outputPath, output, 'utf8'); console.log(`✅ Generated ${outputPath}`); return groups.reduce((total, group) => total + group.properties.length, 0); } /** * Generate an AsciiDoc fragment listing deprecated properties and write it to disk. * * Scans the provided properties map for entries with `is_deprecated === true`, groups * them by `config_scope` ("broker" and "cluster"), sorts each group by property name, * renders the `deprecated-properties` Handlebars template, and writes the output to * `<outputDir>/deprecated/partials/deprecated-properties.adoc`. * * @param {Object.<string, Object>} properties - Map of property objects keyed by property name. * Each property object may contain `is_deprecated`, `config_scope`, and `name` fields. * @param {string} outputDir - Destination directory where the deprecated fragment will be written. * @returns {number} The total number of deprecated properties found and written. */ function generateDeprecatedDocs(properties, outputDir) { const templatePath = getTemplatePath( path.join(__dirname, 'templates', 'deprecated-properties.hbs'), 'TEMPLATE_DEPRECATED' ); const template = handlebars.compile(fs.readFileSync(templatePath, 'utf8')); const deprecatedProperties = Object.values(properties).filter(prop => prop.is_deprecated); const brokerProperties = deprecatedProperties .filter(prop => prop.config_scope === 'broker') .sort((a, b) => String(a.name || '').localeCompare(String(b.name || ''))); const clusterProperties = deprecatedProperties .filter(prop => prop.config_scope === 'cluster') .sort((a, b) => String(a.name || '').localeCompare(String(b.name || ''))); const data = { deprecated: deprecatedProperties.length > 0, brokerProperties: brokerProperties.length > 0 ? brokerProperties : null, clusterProperties: clusterProperties.length > 0 ? clusterProperties : null }; const output = template(data); const outputPath = path.join(outputDir, 'deprecated', 'partials', 'deprecated-properties.adoc'); fs.mkdirSync(path.dirname(outputPath), { recursive: true }); fs.writeFileSync(outputPath, output, 'utf8'); console.log(`✅ Generated ${outputPath}`); return deprecatedProperties.length; } /** * Determine whether any property includes cloud support metadata. * * Checks the provided map of properties and returns true if at least one * property object has a `cloud_supported` own property (regardless of its value). * * @param {Object<string, Object>} properties - Map from property name to its metadata object. * @return {boolean} True if any property has a `cloud_supported` attribute; otherwise false. */ function hasCloudSupportMetadata(properties) { return Object.values(properties).some(prop => Object.prototype.hasOwnProperty.call(prop, 'cloud_supported') ); } /** * Generate all property documentation and write output files to disk. * * Reads properties from the provided JSON file, detects whether any property * includes cloud support metadata to select cloud-aware templates, registers * Handlebars partials accordingly, renders per-type property pages and a * deprecated-properties partial, writes a flat list of all property names, and * produces error reports. * * Generated artifacts are written under the given output directory (e.g.: * pages/<type>/*.adoc, pages/deprecated/partials/deprecated-properties.adoc, * all_properties.txt, and files under outputDir/error). * * @param {string} inputFile - Filesystem path to the input JSON containing a top-level `properties` object. * @param {string} outputDir - Destination directory where generated pages and reports will be written. * @returns {{totalProperties: number, brokerProperties: number, clusterProperties: number, objectStorageProperties: number, topicProperties: number, deprecatedProperties: number}} Summary counts for all properties and per-type totals. */ function generateAllDocs(inputFile, outputDir) { // Read input JSON const data = JSON.parse(fs.readFileSync(inputFile, 'utf8')); const properties = data.properties || {}; // Check if cloud support is enabled const hasCloudSupport = hasCloudSupportMetadata(properties); if (hasCloudSupport) { console.log('🌤️ Cloud support metadata detected, using cloud-aware templates'); } // Register partials with cloud support detection registerPartials(hasCloudSupport); let totalProperties = 0; let totalBrokerProperties = 0; let totalClusterProperties = 0; let totalObjectStorageProperties = 0; let totalTopicProperties = 0; // Generate each type of documentation for (const [type, config] of Object.entries(PROPERTY_CONFIG)) { const count = generatePropertyDocs(properties, config, path.join(outputDir, 'pages')); totalProperties += count; if (type === 'broker') totalBrokerProperties = count; else if (type === 'cluster') totalClusterProperties = count; else if (type === 'object-storage') totalObjectStorageProperties = count; else if (type === 'topic') totalTopicProperties = count; } // Generate deprecated properties documentation const deprecatedCount = generateDeprecatedDocs(properties, path.join(outputDir, 'pages')); // Generate summary file const allPropertiesContent = Object.keys(properties).sort().join('\n'); fs.writeFileSync(path.join(outputDir, 'all_properties.txt'), allPropertiesContent, 'utf8'); // Generate error reports generateErrorReports(properties, outputDir); console.log(`📊 Generation Summary:`); console.log(` Total properties read: ${Object.keys(properties).length}`); console.log(` Total Broker properties: ${totalBrokerProperties}`); console.log(` Total Cluster properties: ${totalClusterProperties}`); console.log(` Total Object Storage properties: ${totalObjectStorageProperties}`); console.log(` Total Topic properties: ${totalTopicProperties}`); console.log(` Total Deprecated properties: ${deprecatedCount}`); return { totalProperties: Object.keys(properties).length, brokerProperties: totalBrokerProperties, clusterProperties: totalClusterProperties, objectStorageProperties: totalObjectStorageProperties, topicProperties: totalTopicProperties, deprecatedProperties: deprecatedCount }; } /** * Generate error reports for properties with missing or invalid data */ function generateErrorReports(properties, outputDir) { const errorDir = path.join(outputDir, 'error'); fs.mkdirSync(errorDir, { recursive: true }); const emptyDescriptions = []; const deprecatedProperties = []; Object.values(properties).forEach(prop => { if (!prop.description || prop.description.trim() === '') { emptyDescriptions.push(prop.name); } if (prop.is_deprecated) { deprecatedProperties.push(prop.name); } }); // Write error reports const totalProperties = Object.keys(properties).length; if (emptyDescriptions.length > 0) { fs.writeFileSync( path.join(errorDir, 'empty_description.txt'), emptyDescriptions.join('\n'), 'utf8' ); const percentage = totalProperties > 0 ? ((emptyDescriptions.length / totalProperties) * 100).toFixed(2) : '0.00'; console.log(`You have ${emptyDescriptions.length} properties with empty description. Percentage of errors: ${percentage}%. Data written in 'empty_description.txt'.`); } if (deprecatedProperties.length > 0) { fs.writeFileSync( path.join(errorDir, 'deprecated_properties.txt'), deprecatedProperties.join('\n'), 'utf8' ); const percentage = totalProperties > 0 ? ((deprecatedProperties.length / totalProperties) * 100).toFixed(2) : '0.00'; console.log(`You have ${deprecatedProperties.length} deprecated properties. Percentage of errors: ${percentage}%. Data written in 'deprecated_properties.txt'.`); } } module.exports = { generateAllDocs, generatePropertyDocs, generateDeprecatedDocs, PROPERTY_CONFIG }; // CLI interface if (require.main === module) { const args = process.argv.slice(2); if (args.length < 2) { console.error('Usage: node generate-handlebars-docs.js <input-file> <output-dir>'); process.exit(1); } const [inputFile, outputDir] = args; if (!fs.existsSync(inputFile)) { console.error(`❌ Input file not found: ${inputFile}`); process.exit(1); } try { generateAllDocs(inputFile, outputDir); console.log('✅ Documentation generation completed successfully'); } catch (error) { console.error(`❌ Error generating documentation: ${error.message}`); process.exit(1); } }