@redpanda-data/docs-extensions-and-macros 4.6.12 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/doc-tools.js CHANGED
@@ -962,6 +962,63 @@ automation
962
962
  if (tmpClone) fs.rmSync(tmpClone, { recursive: true, force: true });
963
963
  });
964
964
 
965
+ /**
966
+ * Generate Markdown table of cloud regions and tiers from master-data.yaml
967
+ */
968
+ automation
969
+ .command('cloud-regions')
970
+ .description('Generate Markdown table of cloud regions and tiers from GitHub YAML file')
971
+ .option('--output <file>', 'Output file (relative to repo root)', 'cloud-controlplane/x-topics/cloud-regions.md')
972
+ .option('--format <fmt>', 'Output format: md (Markdown) or adoc (AsciiDoc)', 'md')
973
+ .option('--owner <owner>', 'GitHub repository owner', 'redpanda-data')
974
+ .option('--repo <repo>', 'GitHub repository name', 'cloudv2-infra')
975
+ .option('--path <path>', 'Path to YAML file in repository', 'apps/master-data-reconciler/manifests/overlays/production/master-data.yaml')
976
+ .option('--ref <ref>', 'Git reference (branch, tag, or commit SHA)', 'integration')
977
+ .option('--template <path>', 'Path to custom Handlebars template (relative to repo root)')
978
+ .option('--dry-run', 'Print output to stdout instead of writing file')
979
+ .action(async (options) => {
980
+ const { generateCloudRegions } = require('../tools/cloud-regions/generate-cloud-regions.js');
981
+
982
+ try {
983
+ const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
984
+ if (!token) {
985
+ throw new Error('GITHUB_TOKEN environment variable is required to fetch from private cloudv2-infra repo.');
986
+ }
987
+ const fmt = (options.format || 'md').toLowerCase();
988
+ let templatePath = undefined;
989
+ if (options.template) {
990
+ const repoRoot = findRepoRoot();
991
+ templatePath = path.resolve(repoRoot, options.template);
992
+ if (!fs.existsSync(templatePath)) {
993
+ throw new Error(`Custom template not found: ${templatePath}`);
994
+ }
995
+ }
996
+ const out = await generateCloudRegions({
997
+ owner: options.owner,
998
+ repo: options.repo,
999
+ path: options.path,
1000
+ ref: options.ref,
1001
+ format: fmt,
1002
+ token,
1003
+ template: templatePath,
1004
+ });
1005
+ if (options.dryRun) {
1006
+ process.stdout.write(out);
1007
+ console.log(`\n✅ (dry-run) ${fmt === 'adoc' ? 'AsciiDoc' : 'Markdown'} output printed to stdout.`);
1008
+ } else {
1009
+ // Always resolve output relative to repo root
1010
+ const repoRoot = findRepoRoot();
1011
+ const absOutput = path.resolve(repoRoot, options.output);
1012
+ fs.mkdirSync(path.dirname(absOutput), { recursive: true });
1013
+ fs.writeFileSync(absOutput, out, 'utf8');
1014
+ console.log(`✅ Wrote ${absOutput}`);
1015
+ }
1016
+ } catch (err) {
1017
+ console.error(`❌ Failed to generate cloud regions: ${err.message}`);
1018
+ process.exit(1);
1019
+ }
1020
+ });
1021
+
965
1022
  automation
966
1023
  .command('crd-spec')
967
1024
  .description('Generate Asciidoc documentation for Kubernetes CRD references')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "4.6.12",
3
+ "version": "4.7.0",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",
@@ -0,0 +1,39 @@
1
+ {{!-- AsciiDoc Cloud Regions Table Template --}}
2
+
3
+ ////
4
+ This content is auto-generated. Do not edit manually.
5
+
6
+ To regenerate this content, run:
7
+ npx doc-tools generate cloud-regions --help
8
+
9
+ For source code and documentation:
10
+ - Source: https://github.com/redpanda-data/docs-extensions-and-macros/tree/main/tools/cloud-regions
11
+ - Docs: https://redpandadata.atlassian.net/wiki/spaces/DOC/pages/1185054748/Doc+Tools+CLI
12
+ ////
13
+
14
+ Usage tiers define the sizing of a cluster and provide tested and guaranteed workload configurations for throughput, logical partitions, and connections. Availability depends on the region and the cluster type (BYOC, Dedicated). See link:https://docs.redpanda.com/redpanda-cloud/reference/tiers/byoc-tiers/[BYOC tiers] and link:https://docs.redpanda.com/redpanda-cloud/reference/tiers/dedicated-tiers/[Dedicated tiers] for further details.
15
+
16
+ {{!--{{#if lastUpdated}}
17
+ _Last Updated: {{lastUpdated}}_
18
+ {{/if}}--}}
19
+
20
+ {{#each providers}}
21
+
22
+ === {{name}}
23
+ .Regions for {{name}}
24
+ [%collapsible]
25
+ ====
26
+ [cols="1,1,2",options="header"]
27
+ |===
28
+ |Region
29
+ |Zones
30
+ |Throughput Tiers
31
+ {{#each regions}}
32
+ |{{name}}
33
+ |{{zones}}
34
+ |{{#each tiers}}* {{this}}
35
+ {{/each}}
36
+ {{/each}}
37
+ |===
38
+ ====
39
+ {{/each}}
@@ -0,0 +1,46 @@
1
+ {{!-- Markdown Cloud Regions Table Template --}}
2
+
3
+ <!--
4
+ This content is auto-generated. Do not edit manually.
5
+
6
+ To regenerate this content, run:
7
+ npx doc-tools generate cloud-regions -h
8
+
9
+ For source code and documentation:
10
+ - Source: https://github.com/redpanda-data/docs-extensions-and-macros/tree/main/tools/cloud-regions
11
+ - Docs: https://redpandadata.atlassian.net/wiki/spaces/DOC/pages/1185054748/Doc+Tools+CLI
12
+ -->
13
+
14
+ Usage tiers define the sizing of a cluster and provide tested and guaranteed workload configurations for throughput, logical partitions, and connections. Availability depends on the region and the cluster type (BYOC, Dedicated). See [BYOC tiers](https://docs.redpanda.com/redpanda-cloud/reference/tiers/byoc-tiers/) and [Dedicated tiers](https://docs.redpanda.com/redpanda-cloud/reference/tiers/dedicated-tiers/) for further details.
15
+
16
+ {{!--{{#if lastUpdated}}
17
+ <p><em>Last Updated: {{lastUpdated}}</em></p>
18
+ {{/if}}--}}
19
+
20
+ {{#each providers}}
21
+ <h3>{{name}}</h3>
22
+ <details><summary>Regions for {{name}}</summary>
23
+ <p class="sr-only">Table of cloud regions and throughput tiers for {{name}}. Use the table headers for more information.</p>
24
+ <table aria-label="Cloud regions and throughput tiers for {{name}}">
25
+ <tr>
26
+ <th title="Cloud region name (such as us-central1)">Region</th>
27
+ <th title="Availability zones in the region (comma-separated)">Zones</th>
28
+ <th title="Available throughput tiers and cluster types in this region">Throughput Tiers</th>
29
+ </tr>
30
+ {{#each regions}}
31
+ <tr>
32
+ <td>{{name}}</td>
33
+ <td>{{zones}}</td>
34
+ <td>
35
+ <ul>
36
+ {{#each tiers}}
37
+ <li>{{this}}</li>
38
+ {{/each}}
39
+ </ul>
40
+ </td>
41
+ </tr>
42
+ {{/each}}
43
+ </table>
44
+
45
+ </details>
46
+ {{/each}}
@@ -0,0 +1,243 @@
1
+ // Standalone module to fetch, parse, filter, and render cloud regions/tier data
2
+ // Usage: generateCloudRegions({ sourceUrl, format, token })
3
+
4
+ /**
5
+ * Expected YAML source data shape:
6
+ *
7
+ * {
8
+ * regions: [
9
+ * {
10
+ * name: string, // Region name, such as "us-west1"
11
+ * cloudProvider: string, // One of CLOUD_PROVIDER_AWS, CLOUD_PROVIDER_GCP, CLOUD_PROVIDER_AZURE
12
+ * zones: [string] | string, // List of zones or comma-separated string
13
+ * redpandaProductAvailability: {
14
+ * [key: string]: {
15
+ * redpandaProductName: string, // Product name (must match a public product in products[])
16
+ * clusterTypes: [string], // List of cluster type enums
17
+ * }
18
+ * }
19
+ * }, ...
20
+ * ],
21
+ * products: [
22
+ * {
23
+ * name: string, // Product name (used for filtering and output)
24
+ * isPublic: boolean, // Only public products are documented
25
+ * }, ...
26
+ * ]
27
+ * }
28
+ *
29
+ * All keys are required unless otherwise noted. Only public products/tiers are included in output.
30
+ */
31
+
32
+ const path = require('path');
33
+ const fs = require('fs');
34
+ const jsYaml = require('js-yaml');
35
+ const renderCloudRegions = require('./render-cloud-regions');
36
+
37
+ const providerMap = {
38
+ CLOUD_PROVIDER_AWS: 'AWS',
39
+ CLOUD_PROVIDER_GCP: 'GCP',
40
+ CLOUD_PROVIDER_AZURE: 'Azure',
41
+ };
42
+ const providerOrder = ['GCP', 'AWS', 'Azure'];
43
+ const clusterTypeMap = {
44
+ CLUSTER_TYPE_BYOC: 'BYOC',
45
+ CLUSTER_TYPE_DEDICATED: 'Dedicated',
46
+ CLUSTER_TYPE_FMC: 'Dedicated',
47
+ };
48
+ /**
49
+ * Returns the display name for a given cluster type, or the original value if unmapped.
50
+ * @param {string} ct - The internal cluster type identifier.
51
+ * @return {string} The display name for the cluster type.
52
+ */
53
+ function displayClusterType(ct) {
54
+ return clusterTypeMap[ct] || ct;
55
+ }
56
+
57
+ /**
58
+ * Fetches YAML content from GitHub using the GitHub API.
59
+ *
60
+ * Uses the GitHub API to fetch file content, which avoids caching issues that can occur with raw URLs.
61
+ *
62
+ * @param {Object} options - Options for fetching the YAML content.
63
+ * @param {string} options.owner - GitHub repository owner.
64
+ * @param {string} options.repo - GitHub repository name.
65
+ * @param {string} options.path - Path to the file within the repository.
66
+ * @param {string} [options.ref='main'] - Git reference (branch, tag, or commit SHA).
67
+ * @param {string} [options.token] - Optional GitHub token for authorization.
68
+ * @returns {Promise<string>} The fetched YAML content as a string.
69
+ * @throws {Error} If the GitHub API call fails or the file cannot be found.
70
+ */
71
+ async function fetchYaml({ owner, repo, path, ref = 'main', token }) {
72
+ try {
73
+ const { Octokit } = await import('@octokit/rest');
74
+ const octokit = new Octokit(token ? { auth: token } : {});
75
+
76
+ console.log(`[cloud-regions] INFO: Fetching ${owner}/${repo}/${path}@${ref} via GitHub API`);
77
+
78
+ const response = await octokit.repos.getContent({
79
+ owner,
80
+ repo,
81
+ path,
82
+ ref,
83
+ });
84
+
85
+ if (Array.isArray(response.data)) {
86
+ throw new Error(`Path ${path} is a directory, not a file`);
87
+ }
88
+
89
+ if (response.data.type !== 'file') {
90
+ throw new Error(`Path ${path} is not a file`);
91
+ }
92
+
93
+ // Decode base64 content
94
+ const content = Buffer.from(response.data.content, 'base64').toString('utf8');
95
+
96
+ if (!content || content.trim() === '') {
97
+ throw new Error('Empty YAML content received from GitHub API');
98
+ }
99
+
100
+ return content;
101
+ } catch (err) {
102
+ console.error(`[cloud-regions] ERROR: Failed to fetch from GitHub API: ${err.message}`);
103
+ throw new Error(`GitHub API fetch failed: ${err.message}`);
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Parses YAML text describing cloud regions and products, filters for public products, and organizes regions by provider.
109
+ *
110
+ * The function expects YAML content with a top-level `regions` array and an optional `products` array. It groups regions by cloud provider, includes only those with at least one public product tier, and formats tier and cluster type information for each region. Providers and regions without public tiers are excluded from the result.
111
+ *
112
+ * @param {string} yamlText - The YAML content to parse and process.
113
+ * @return {Array<Object>} An array of provider objects, each containing a name and a list of regions with their available public product tiers.
114
+ * @throws {Error} If the YAML is malformed or missing the required `regions` array.
115
+ */
116
+ function processCloudRegions(yamlText) {
117
+ let data;
118
+ try {
119
+ data = jsYaml.load(yamlText);
120
+ } catch (e) {
121
+ console.error('[cloud-regions] ERROR: Malformed YAML.');
122
+ throw new Error('Malformed YAML: ' + e.message);
123
+ }
124
+ if (!data || !Array.isArray(data.regions)) {
125
+ console.error('[cloud-regions] ERROR: YAML missing top-level regions array.');
126
+ throw new Error('YAML does not contain a top-level regions array.');
127
+ }
128
+ // Ensure grouped keys match providerOrder and providerMap values
129
+ const grouped = { AWS: [], GCP: [], Azure: [] };
130
+ for (const region of data.regions) {
131
+ const key = providerMap[region.cloudProvider];
132
+ if (!key) {
133
+ console.warn(`[cloud-regions] WARN: Unknown cloudProvider '${region.cloudProvider}' in region '${region.name}'. Skipping.`);
134
+ continue;
135
+ }
136
+ grouped[key].push(region);
137
+ }
138
+ // Build a set of public product names
139
+ const publicProductNames = new Set();
140
+ if (Array.isArray(data.products)) {
141
+ for (const product of data.products) {
142
+ if (product.isPublic && product.name) {
143
+ publicProductNames.add(product.name);
144
+ }
145
+ }
146
+ } else {
147
+ console.warn('[cloud-regions] WARN: No products array found in YAML.');
148
+ }
149
+ // Prepare providers array for template, only including public products and using name
150
+ const providers = providerOrder
151
+ .filter((prov) => grouped[prov] && grouped[prov].length > 0)
152
+ .map((prov) => {
153
+ // Only include regions that have at least one public product/tier
154
+ const filteredRegions = grouped[prov].map((region) => {
155
+ const zones = Array.isArray(region.zones) ? region.zones.join(',') : (region.zones || '');
156
+ let tiers = [];
157
+ if (region.redpandaProductAvailability && typeof region.redpandaProductAvailability === 'object') {
158
+ // Group by tier name, collect all cluster types for that tier
159
+ const tierMap = {};
160
+ for (const t of Object.values(region.redpandaProductAvailability)) {
161
+ if (!t.redpandaProductName || !publicProductNames.has(t.redpandaProductName)) {
162
+ continue;
163
+ }
164
+ const productName = t.redpandaProductName;
165
+ if (!tierMap[productName]) tierMap[productName] = new Set();
166
+ if (Array.isArray(t.clusterTypes)) {
167
+ for (const ct of t.clusterTypes) tierMap[productName].add(displayClusterType(ct));
168
+ }
169
+ }
170
+ tiers = Object.entries(tierMap)
171
+ .map(([productName, cts]) => `${productName}: ${Array.from(cts).sort().join(', ')}`)
172
+ .sort((a, b) => a.localeCompare(b));
173
+ }
174
+ return {
175
+ name: region.name,
176
+ zones,
177
+ tiers,
178
+ };
179
+ }).filter(region => region.tiers && region.tiers.length > 0);
180
+ if (filteredRegions.length === 0) {
181
+ console.info(`[cloud-regions] INFO: No public tiers found for provider '${prov}'.`);
182
+ }
183
+ return {
184
+ name: prov,
185
+ regions: filteredRegions,
186
+ };
187
+ })
188
+ .filter(provider => provider.regions && provider.regions.length > 0);
189
+ if (providers.length === 0) {
190
+ console.warn('[cloud-regions] WARN: No providers/regions found after filtering.');
191
+ }
192
+ return providers;
193
+ }
194
+
195
+ /**
196
+ * Fetches, processes, and renders cloud region and tier data from a GitHub YAML file.
197
+ *
198
+ * Retrieves YAML data from GitHub using the GitHub API (to avoid caching issues),
199
+ * parses and filters it to include only public cloud regions and tiers, and renders the result in the requested format.
200
+ *
201
+ * @param {Object} options - Options for generating cloud regions.
202
+ * @param {string} options.owner - GitHub repository owner.
203
+ * @param {string} options.repo - GitHub repository name.
204
+ * @param {string} options.path - Path to the YAML file within the repository.
205
+ * @param {string} [options.ref='main'] - Git reference (branch, tag, or commit SHA).
206
+ * @param {string} [options.format='md'] - The output format (e.g., 'md' for Markdown).
207
+ * @param {string} [options.token] - Optional GitHub token for authentication.
208
+ * @param {string} [options.template] - Optional path to custom Handlebars template.
209
+ * @returns {string} The rendered cloud regions output.
210
+ * @throws {Error} If fetching, processing, or rendering fails, or if no valid providers or regions are found.
211
+ */
212
+ async function generateCloudRegions({ owner, repo, path, ref = 'main', format = 'md', token, template }) {
213
+ let yamlText;
214
+ try {
215
+ yamlText = await fetchYaml({ owner, repo, path, ref, token });
216
+ } catch (err) {
217
+ console.error(`[cloud-regions] ERROR: Failed to fetch YAML: ${err.message}`);
218
+ throw err;
219
+ }
220
+ let providers;
221
+ try {
222
+ providers = processCloudRegions(yamlText);
223
+ } catch (err) {
224
+ console.error(`[cloud-regions] ERROR: Failed to process cloud regions: ${err.message}`);
225
+ throw err;
226
+ }
227
+ if (providers.length === 0) {
228
+ console.error('[cloud-regions] ERROR: No providers/regions found in YAML after filtering.');
229
+ throw new Error('No providers/regions found in YAML after filtering.');
230
+ }
231
+ const lastUpdated = new Date().toISOString();
232
+ try {
233
+ return renderCloudRegions({ providers, format, lastUpdated, template });
234
+ } catch (err) {
235
+ console.error(`[cloud-regions] ERROR: Failed to render cloud regions: ${err.message}`);
236
+ throw err;
237
+ }
238
+ }
239
+
240
+ module.exports = {
241
+ generateCloudRegions,
242
+ processCloudRegions,
243
+ };
@@ -0,0 +1,48 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const handlebars = require('handlebars');
4
+
5
+
6
+ /**
7
+ * Generates a formatted string representing cloud provider regions using a Handlebars template.
8
+ *
9
+ * Sorts regions alphabetically within each provider and renders the data using a template file corresponding to the specified format ('md' or 'adoc'). Optionally includes a last updated timestamp.
10
+ *
11
+ * @param {Object} opts - Options for rendering.
12
+ * @param {Array} opts.providers - List of cloud provider objects, each with a name and an array of regions.
13
+ * @param {string} opts.format - Output format, either 'md' (Markdown) or 'adoc' (AsciiDoc).
14
+ * @param {string} [opts.lastUpdated] - Optional ISO timestamp indicating when the data was last updated.
15
+ * @returns {string} The rendered output string.
16
+ * @throws {Error} If the providers array is missing or empty.
17
+ */
18
+ function renderCloudRegions({ providers, format, lastUpdated }) {
19
+ if (!Array.isArray(providers) || providers.length === 0) {
20
+ throw new Error('No providers/regions found in YAML.');
21
+ }
22
+ if (!['md', 'adoc'].includes(format)) {
23
+ throw new Error(`Unsupported format: ${format}. Use 'md' or 'adoc'.`);
24
+ }
25
+ // Sort regions alphabetically within each provider
26
+ const sortedProviders = providers.map(provider => ({
27
+ ...provider,
28
+ regions: [...provider.regions].sort((a, b) => a.name.localeCompare(b.name))
29
+ }));
30
+ const templateFile = path.join(__dirname, `cloud-regions-table-${format}.hbs`);
31
+ if (!fs.existsSync(templateFile)) {
32
+ throw new Error(`Template file not found: ${templateFile}`);
33
+ }
34
+ let templateSrc, template;
35
+ try {
36
+ templateSrc = fs.readFileSync(templateFile, 'utf8');
37
+ template = handlebars.compile(templateSrc);
38
+ } catch (err) {
39
+ throw new Error(`Failed to compile Handlebars template at ${templateFile}: ${err.message}`);
40
+ }
41
+ try {
42
+ return template({ providers: sortedProviders, lastUpdated });
43
+ } catch (err) {
44
+ throw new Error(`Failed to render Handlebars template at ${templateFile}: ${err.message}`);
45
+ }
46
+ }
47
+
48
+ module.exports = renderCloudRegions;
@@ -67,7 +67,11 @@ function mergeOverrides(target, overrides) {
67
67
  }
68
68
 
69
69
  // === Handle examples ===
70
- if (key === 'examples' && Array.isArray(overrides[key]) && Array.isArray(target[key])) {
70
+ if (key === 'examples' && Array.isArray(overrides[key])) {
71
+ // If target[key] is not an array, initialize it
72
+ if (!Array.isArray(target[key])) {
73
+ target[key] = [];
74
+ }
71
75
  const overrideMap = new Map(overrides[key].map(o => [o.title, o]));
72
76
 
73
77
  target[key] = target[key].map(example => {