@redpanda-data/docs-extensions-and-macros 5.13.1 → 5.13.2
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-mcp.js +6 -2
- package/bin/doc-tools.js +40 -15
- package/bin/mcp-tools/cloud-regions.js +8 -2
- package/cli-utils/doc-tools-utils.js +54 -0
- package/mcp/CLI_INTERFACE.adoc +4 -3
- package/package.json +1 -1
- package/tools/cloud-regions/cloud-regions-table-adoc.hbs +1 -1
- package/tools/cloud-regions/cloud-regions-table-md.hbs +1 -1
- package/tools/cloud-regions/generate-cloud-regions.js +110 -16
- package/tools/cloud-regions/render-cloud-regions.js +12 -6
package/bin/doc-tools-mcp.js
CHANGED
|
@@ -291,7 +291,7 @@ const tools = [
|
|
|
291
291
|
properties: {
|
|
292
292
|
output: {
|
|
293
293
|
type: 'string',
|
|
294
|
-
description: 'Output file path relative to repo root (optional, defaults to "cloud-controlplane/x-topics/cloud-regions.md")'
|
|
294
|
+
description: 'Output file path relative to repo root; must stay inside the repository (optional, defaults to "cloud-controlplane/x-topics/cloud-regions.md")'
|
|
295
295
|
},
|
|
296
296
|
format: {
|
|
297
297
|
type: 'string',
|
|
@@ -316,7 +316,11 @@ const tools = [
|
|
|
316
316
|
},
|
|
317
317
|
template: {
|
|
318
318
|
type: 'string',
|
|
319
|
-
description: 'Path to custom Handlebars template relative to repo root (optional)'
|
|
319
|
+
description: 'Path to custom Handlebars template relative to repo root; must stay inside the repository (optional)'
|
|
320
|
+
},
|
|
321
|
+
cluster_type: {
|
|
322
|
+
type: 'string',
|
|
323
|
+
description: 'Only include regions/tiers available for this cluster type, usually "BYOC" or "Dedicated"; the CLI validates the value against the cluster types the source data offers and lists them if it does not match (optional, requires output or dry_run)'
|
|
320
324
|
},
|
|
321
325
|
dry_run: {
|
|
322
326
|
type: 'boolean',
|
package/bin/doc-tools.js
CHANGED
|
@@ -12,7 +12,7 @@ const path = require('path')
|
|
|
12
12
|
const fs = require('fs')
|
|
13
13
|
|
|
14
14
|
// Import extracted utility modules
|
|
15
|
-
const { findRepoRoot, fail, commonOptions } = require('../cli-utils/doc-tools-utils')
|
|
15
|
+
const { findRepoRoot, resolveInsideRepo, fail, commonOptions } = require('../cli-utils/doc-tools-utils')
|
|
16
16
|
const {
|
|
17
17
|
requireTool,
|
|
18
18
|
requireCmd,
|
|
@@ -1827,6 +1827,14 @@ automation
|
|
|
1827
1827
|
* generates either Markdown or AsciiDoc tables for documentation. Supports custom templates
|
|
1828
1828
|
* and dry-run mode for previewing output.
|
|
1829
1829
|
*
|
|
1830
|
+
* A custom template is a Handlebars file inside this repository and receives this context:
|
|
1831
|
+
* `providers`, each with `name` ("GCP"), `displayName` ("Google Cloud Platform (GCP)") and
|
|
1832
|
+
* `regions`, each region with `name`, `zones` (comma separated) and `tiers`; `clusterType`,
|
|
1833
|
+
* the cluster type the data was filtered to, set only with --cluster-type, in which case the
|
|
1834
|
+
* tier entries carry no per-tier cluster type; and `lastUpdated`, an ISO timestamp. The
|
|
1835
|
+
* bundled templates in tools/cloud-regions show all of it in use. The template decides the
|
|
1836
|
+
* markup, so with --template the --format value is only checked for validity.
|
|
1837
|
+
*
|
|
1830
1838
|
* @why
|
|
1831
1839
|
* Cloud region data changes frequently as new regions are added and tier availability evolves.
|
|
1832
1840
|
* The cloudv2-infra repository contains the source of truth for cloud infrastructure. Manual
|
|
@@ -1852,6 +1860,12 @@ automation
|
|
|
1852
1860
|
* npx doc-tools generate cloud-regions \
|
|
1853
1861
|
* --output custom/path/regions.md
|
|
1854
1862
|
*
|
|
1863
|
+
* # Generate an AsciiDoc partial for one cluster type with a custom template
|
|
1864
|
+
* export GITHUB_TOKEN=ghp_xxx
|
|
1865
|
+
* npx doc-tools generate cloud-regions --format adoc --cluster-type BYOC \
|
|
1866
|
+
* --template docs-data/templates/cloud-regions.hbs \
|
|
1867
|
+
* --output modules/reference/partials/generated/regions-byoc.adoc
|
|
1868
|
+
*
|
|
1855
1869
|
* # Use different branch for testing
|
|
1856
1870
|
* export GITHUB_TOKEN=ghp_xxx
|
|
1857
1871
|
* npx doc-tools generate cloud-regions --ref staging
|
|
@@ -1864,32 +1878,44 @@ automation
|
|
|
1864
1878
|
automation
|
|
1865
1879
|
.command('cloud-regions')
|
|
1866
1880
|
.description('Generate Markdown table of cloud regions and tiers from GitHub YAML file')
|
|
1867
|
-
.option('--output <file>', 'Output file (relative to repo root)', 'cloud-controlplane/x-topics/cloud-regions.md')
|
|
1881
|
+
.option('--output <file>', 'Output file (relative to repo root, must stay inside the repository)', 'cloud-controlplane/x-topics/cloud-regions.md')
|
|
1868
1882
|
.option('--format <fmt>', 'Output format: md (Markdown) or adoc (AsciiDoc)', 'md')
|
|
1869
1883
|
.option('--owner <owner>', 'GitHub repository owner', 'redpanda-data')
|
|
1870
1884
|
.option('--repo <repo>', 'GitHub repository name', 'cloudv2-infra')
|
|
1871
1885
|
.option('--path <path>', 'Path to YAML file in repository', 'apps/master-data-reconciler/manifests/overlays/production/master-data.yaml')
|
|
1872
1886
|
.option('--ref <ref>', 'Git reference (branch, tag, or commit SHA)', 'integration')
|
|
1873
|
-
.option('--template <path>', 'Path to custom Handlebars template (relative to repo root)')
|
|
1887
|
+
.option('--template <path>', 'Path to custom Handlebars template (relative to repo root, must stay inside the repository)')
|
|
1888
|
+
.option('--cluster-type <type>', 'Only include regions/tiers available for this cluster type, such as BYOC or Dedicated (requires --output or --dry-run)')
|
|
1874
1889
|
.option('--dry-run', 'Print output to stdout instead of writing file')
|
|
1875
|
-
.action(async (options) => {
|
|
1890
|
+
.action(async (options, command) => {
|
|
1876
1891
|
const { generateCloudRegions } = require('../tools/cloud-regions/generate-cloud-regions.js')
|
|
1877
1892
|
const { getGitHubToken } = require('../cli-utils/github-token')
|
|
1878
1893
|
|
|
1879
1894
|
try {
|
|
1895
|
+
// The default output path holds the unfiltered table, so a filtered run
|
|
1896
|
+
// that forgets --output silently replaces it, and the documented
|
|
1897
|
+
// BYOC-then-Dedicated pair would leave only whichever ran last.
|
|
1898
|
+
if (options.clusterType && !options.dryRun && command.getOptionValueSource('output') === 'default') {
|
|
1899
|
+
throw new Error('--cluster-type needs its own destination: pass --output <file> for the filtered table, or --dry-run to preview it.')
|
|
1900
|
+
}
|
|
1901
|
+
// Contain every caller-supplied path before doing any work: both options
|
|
1902
|
+
// are also reachable through the MCP server, so --template must not read
|
|
1903
|
+
// and --output must not write outside the repository.
|
|
1904
|
+
const repoRoot = findRepoRoot()
|
|
1905
|
+
const templatePath = options.template
|
|
1906
|
+
? resolveInsideRepo(repoRoot, options.template, '--template')
|
|
1907
|
+
: undefined
|
|
1908
|
+
if (templatePath && !fs.existsSync(templatePath)) {
|
|
1909
|
+
throw new Error(`Custom template not found: ${templatePath}`)
|
|
1910
|
+
}
|
|
1911
|
+
const absOutput = options.dryRun
|
|
1912
|
+
? undefined
|
|
1913
|
+
: resolveInsideRepo(repoRoot, options.output, '--output')
|
|
1880
1914
|
const token = getGitHubToken()
|
|
1881
1915
|
if (!token) {
|
|
1882
1916
|
throw new Error('GitHub token is required to fetch from private cloudv2-infra repo.')
|
|
1883
1917
|
}
|
|
1884
1918
|
const fmt = (options.format || 'md').toLowerCase()
|
|
1885
|
-
let templatePath
|
|
1886
|
-
if (options.template) {
|
|
1887
|
-
const repoRoot = findRepoRoot()
|
|
1888
|
-
templatePath = path.resolve(repoRoot, options.template)
|
|
1889
|
-
if (!fs.existsSync(templatePath)) {
|
|
1890
|
-
throw new Error(`Custom template not found: ${templatePath}`)
|
|
1891
|
-
}
|
|
1892
|
-
}
|
|
1893
1919
|
const out = await generateCloudRegions({
|
|
1894
1920
|
owner: options.owner,
|
|
1895
1921
|
repo: options.repo,
|
|
@@ -1897,14 +1923,13 @@ automation
|
|
|
1897
1923
|
ref: options.ref,
|
|
1898
1924
|
format: fmt,
|
|
1899
1925
|
token,
|
|
1900
|
-
template: templatePath
|
|
1926
|
+
template: templatePath,
|
|
1927
|
+
clusterType: options.clusterType
|
|
1901
1928
|
})
|
|
1902
1929
|
if (options.dryRun) {
|
|
1903
1930
|
process.stdout.write(out)
|
|
1904
1931
|
console.log(`\nDone: (dry-run) ${fmt === 'adoc' ? 'AsciiDoc' : 'Markdown'} output printed to stdout.`)
|
|
1905
1932
|
} else {
|
|
1906
|
-
const repoRoot = findRepoRoot()
|
|
1907
|
-
const absOutput = path.resolve(repoRoot, options.output)
|
|
1908
1933
|
fs.mkdirSync(path.dirname(absOutput), { recursive: true })
|
|
1909
1934
|
fs.writeFileSync(absOutput, out, 'utf8')
|
|
1910
1935
|
console.log(`Done: Wrote ${absOutput}`)
|
|
@@ -9,13 +9,14 @@ const { getAntoraStructure } = require('./antora');
|
|
|
9
9
|
/**
|
|
10
10
|
* Generate cloud regions table documentation
|
|
11
11
|
* @param {Object} args - Arguments
|
|
12
|
-
* @param {string} [args.output] - Output file path (relative to repo root)
|
|
12
|
+
* @param {string} [args.output] - Output file path (relative to repo root, must stay inside the repository)
|
|
13
13
|
* @param {string} [args.format] - Output format: 'md' or 'adoc'
|
|
14
14
|
* @param {string} [args.owner] - GitHub repository owner
|
|
15
15
|
* @param {string} [args.repo] - GitHub repository name
|
|
16
16
|
* @param {string} [args.path] - Path to YAML file in repository
|
|
17
17
|
* @param {string} [args.ref] - Git reference (branch, tag, or commit SHA)
|
|
18
|
-
* @param {string} [args.template] - Path to custom Handlebars template
|
|
18
|
+
* @param {string} [args.template] - Path to custom Handlebars template (relative to repo root, must stay inside the repository)
|
|
19
|
+
* @param {string} [args.cluster_type] - Only include regions/tiers available for this cluster type ('BYOC' or 'Dedicated')
|
|
19
20
|
* @param {boolean} [args.dry_run] - Print output to stdout instead of writing file
|
|
20
21
|
* @returns {Object} Generation results
|
|
21
22
|
*/
|
|
@@ -73,6 +74,11 @@ function generateCloudRegions(args = {}) {
|
|
|
73
74
|
baseArgs.push(args.template);
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
if (args.cluster_type) {
|
|
78
|
+
baseArgs.push('--cluster-type');
|
|
79
|
+
baseArgs.push(args.cluster_type);
|
|
80
|
+
}
|
|
81
|
+
|
|
76
82
|
if (args.dry_run) {
|
|
77
83
|
baseArgs.push('--dry-run');
|
|
78
84
|
}
|
|
@@ -28,6 +28,59 @@ function findRepoRoot (start = process.cwd()) {
|
|
|
28
28
|
process.exit(1)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Returns the canonical path with symlinks resolved, or the input unchanged when
|
|
33
|
+
* the path cannot be resolved (for example, it does not exist).
|
|
34
|
+
*
|
|
35
|
+
* @param {string} target - The path to canonicalize.
|
|
36
|
+
* @returns {string} The canonical path, or `target` if it cannot be resolved.
|
|
37
|
+
*/
|
|
38
|
+
function realPathOrSelf (target) {
|
|
39
|
+
try {
|
|
40
|
+
return fs.realpathSync(target)
|
|
41
|
+
} catch (err) {
|
|
42
|
+
return target
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolves a caller-supplied path against the repository root and refuses any
|
|
48
|
+
* path that escapes it.
|
|
49
|
+
*
|
|
50
|
+
* Options such as `--template` and `--output` are resolved with
|
|
51
|
+
* `path.resolve(repoRoot, value)`, which silently keeps an absolute path as-is
|
|
52
|
+
* and lets `../` climb out of the repository. Both are reachable from the MCP
|
|
53
|
+
* server, where the value comes from an agent rather than from the person at the
|
|
54
|
+
* keyboard, so a read option turns into a file-disclosure primitive and a write
|
|
55
|
+
* option into a write-anywhere primitive. Use this helper for every path option
|
|
56
|
+
* that names a file the CLI then reads or writes.
|
|
57
|
+
*
|
|
58
|
+
* Symlinks are resolved on the deepest existing part of the target, so a symlink
|
|
59
|
+
* inside the repository cannot be used to point outside it. The part that does
|
|
60
|
+
* not exist yet (a `--output` file that is about to be created) is checked
|
|
61
|
+
* lexically, after `path.resolve` has already collapsed any `..` segments.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} repoRoot - Absolute path to the repository root.
|
|
64
|
+
* @param {string} userPath - The caller-supplied path, absolute or relative to the repo root.
|
|
65
|
+
* @param {string} [label] - Human-readable name of the option, used in the error message.
|
|
66
|
+
* @returns {string} The absolute, contained path.
|
|
67
|
+
* @throws {Error} If the resolved path is outside the repository root.
|
|
68
|
+
*/
|
|
69
|
+
function resolveInsideRepo (repoRoot, userPath, label = 'path') {
|
|
70
|
+
const resolved = path.resolve(repoRoot, userPath)
|
|
71
|
+
const root = realPathOrSelf(repoRoot)
|
|
72
|
+
let existing = resolved
|
|
73
|
+
while (!fs.existsSync(existing) && path.dirname(existing) !== existing) {
|
|
74
|
+
existing = path.dirname(existing)
|
|
75
|
+
}
|
|
76
|
+
const real = path.join(realPathOrSelf(existing), path.relative(existing, resolved))
|
|
77
|
+
const relative = path.relative(root, real)
|
|
78
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
79
|
+
throw new Error(`${label} must be inside the repository (${repoRoot}): ${userPath}`)
|
|
80
|
+
}
|
|
81
|
+
return resolved
|
|
82
|
+
}
|
|
83
|
+
|
|
31
84
|
/**
|
|
32
85
|
* Prints an error message to stderr and exits the process with a non-zero status.
|
|
33
86
|
*
|
|
@@ -49,6 +102,7 @@ const commonOptions = {
|
|
|
49
102
|
|
|
50
103
|
module.exports = {
|
|
51
104
|
findRepoRoot,
|
|
105
|
+
resolveInsideRepo,
|
|
52
106
|
fail,
|
|
53
107
|
commonOptions
|
|
54
108
|
}
|
package/mcp/CLI_INTERFACE.adoc
CHANGED
|
@@ -203,13 +203,14 @@ npx doc-tools generate cloud-regions [options]
|
|
|
203
203
|
|
|
204
204
|
*Optional flags:*
|
|
205
205
|
|
|
206
|
-
--output:: Output file path
|
|
207
|
-
--format:: Output format (
|
|
206
|
+
--output:: Output file path, relative to repo root and inside the repository
|
|
207
|
+
--format:: Output format (md, adoc)
|
|
208
208
|
--owner:: GitHub repository owner
|
|
209
209
|
--repo:: GitHub repository name
|
|
210
210
|
--path:: Path to regions file in repository
|
|
211
211
|
--ref:: Git reference (branch, tag, commit)
|
|
212
|
-
--template::
|
|
212
|
+
--template:: Handlebars template file, relative to repo root and inside the repository. The template decides the markup, so `--format` is then only checked for validity
|
|
213
|
+
--cluster-type:: Only include regions and tiers available for one cluster type, such as BYOC or Dedicated. Requires `--output` or `--dry-run`
|
|
213
214
|
--dry-run:: Show what would be generated without writing files
|
|
214
215
|
|
|
215
216
|
=== crd-spec
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@ For source code and documentation:
|
|
|
11
11
|
- Docs: https://redpandadata.atlassian.net/wiki/spaces/DOC/pages/1185054748/Doc+Tools+CLI
|
|
12
12
|
////
|
|
13
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.
|
|
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{{#if clusterType}}. This table lists only the tiers available for {{clusterType}} clusters{{else}} and the cluster type (BYOC, Dedicated){{/if}}. 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
15
|
|
|
16
16
|
{{!--{{#if lastUpdated}}
|
|
17
17
|
_Last Updated: {{lastUpdated}}_
|
|
@@ -11,7 +11,7 @@ For source code and documentation:
|
|
|
11
11
|
- Docs: https://redpandadata.atlassian.net/wiki/spaces/DOC/pages/1185054748/Doc+Tools+CLI
|
|
12
12
|
-->
|
|
13
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.
|
|
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{{#if clusterType}}. This table lists only the tiers available for {{clusterType}} clusters{{else}} and the cluster type (BYOC, Dedicated){{/if}}. 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
15
|
|
|
16
16
|
{{!--{{#if lastUpdated}}
|
|
17
17
|
<p><em>Last Updated: {{lastUpdated}}</em></p>
|
|
@@ -40,18 +40,38 @@ const providerMap = {
|
|
|
40
40
|
CLOUD_PROVIDER_AZURE: 'Azure',
|
|
41
41
|
};
|
|
42
42
|
const providerOrder = ['GCP', 'AWS', 'Azure'];
|
|
43
|
+
const providerDisplayNames = {
|
|
44
|
+
GCP: 'Google Cloud Platform (GCP)',
|
|
45
|
+
AWS: 'Amazon Web Services (AWS)',
|
|
46
|
+
Azure: 'Azure',
|
|
47
|
+
};
|
|
43
48
|
const clusterTypeMap = {
|
|
44
49
|
CLUSTER_TYPE_BYOC: 'BYOC',
|
|
45
50
|
CLUSTER_TYPE_DEDICATED: 'Dedicated',
|
|
46
51
|
CLUSTER_TYPE_FMC: 'Dedicated',
|
|
47
52
|
};
|
|
53
|
+
// Warn once per unmapped cluster type per process, so a rename upstream is loud
|
|
54
|
+
// instead of being spread over every region row.
|
|
55
|
+
const warnedClusterTypes = new Set();
|
|
56
|
+
|
|
48
57
|
/**
|
|
49
58
|
* Returns the display name for a given cluster type, or the original value if unmapped.
|
|
59
|
+
*
|
|
60
|
+
* An unmapped value is a signal, not a detail: it reaches the published table
|
|
61
|
+
* verbatim, and if a mapped enum is ever renamed upstream the table it feeds
|
|
62
|
+
* (every Dedicated region comes from CLUSTER_TYPE_FMC) empties out quietly. Warn
|
|
63
|
+
* so that shows up in the generation logs.
|
|
64
|
+
*
|
|
50
65
|
* @param {string} ct - The internal cluster type identifier.
|
|
51
66
|
* @return {string} The display name for the cluster type.
|
|
52
67
|
*/
|
|
53
68
|
function displayClusterType(ct) {
|
|
54
|
-
|
|
69
|
+
if (clusterTypeMap[ct]) return clusterTypeMap[ct];
|
|
70
|
+
if (!warnedClusterTypes.has(ct)) {
|
|
71
|
+
warnedClusterTypes.add(ct);
|
|
72
|
+
console.warn(`[cloud-regions] WARN: Unmapped cluster type '${ct}'. It appears in the table exactly as written here, and --cluster-type has to be given that same value. Add it to clusterTypeMap to give it a display name.`);
|
|
73
|
+
}
|
|
74
|
+
return ct;
|
|
55
75
|
}
|
|
56
76
|
|
|
57
77
|
/**
|
|
@@ -105,15 +125,42 @@ async function fetchYaml({ owner, repo, path, ref = 'main', token }) {
|
|
|
105
125
|
}
|
|
106
126
|
|
|
107
127
|
/**
|
|
108
|
-
*
|
|
128
|
+
* Collects every cluster type display name that the source data offers on a
|
|
129
|
+
* public tier, together with the ones this tool maps by default.
|
|
109
130
|
*
|
|
110
|
-
*
|
|
131
|
+
* @param {Array<Object>} regions - The `regions` array from the source YAML.
|
|
132
|
+
* @param {Set<string>} publicProductNames - Names of products marked public.
|
|
133
|
+
* @return {Set<string>} Cluster type display names available for filtering.
|
|
134
|
+
*/
|
|
135
|
+
function collectClusterTypes(regions, publicProductNames) {
|
|
136
|
+
const available = new Set(Object.values(clusterTypeMap));
|
|
137
|
+
for (const region of regions) {
|
|
138
|
+
const availability = region.redpandaProductAvailability;
|
|
139
|
+
if (!availability || typeof availability !== 'object') continue;
|
|
140
|
+
for (const tier of Object.values(availability)) {
|
|
141
|
+
if (!tier.redpandaProductName || !publicProductNames.has(tier.redpandaProductName)) continue;
|
|
142
|
+
if (!Array.isArray(tier.clusterTypes)) continue;
|
|
143
|
+
for (const ct of tier.clusterTypes) available.add(displayClusterType(ct));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return available;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Parses YAML text describing cloud regions and products and returns both the
|
|
151
|
+
* providers and the cluster type the result was filtered to.
|
|
152
|
+
*
|
|
153
|
+
* Templates need the resolved cluster type, not the string the caller typed, so
|
|
154
|
+
* that a filtered table can name it in a heading or an intro. Keeping the
|
|
155
|
+
* resolution here means the YAML is parsed once.
|
|
111
156
|
*
|
|
112
157
|
* @param {string} yamlText - The YAML content to parse and process.
|
|
113
|
-
* @
|
|
114
|
-
* @
|
|
158
|
+
* @param {Object} [options] - Processing options.
|
|
159
|
+
* @param {string} [options.clusterType] - Optional cluster type display name (case-insensitive).
|
|
160
|
+
* @return {{providers: Array<Object>, clusterType: string|undefined}} The providers and the resolved cluster type filter.
|
|
161
|
+
* @throws {Error} If the YAML is malformed, missing the required `regions` array, or the cluster type is unsupported.
|
|
115
162
|
*/
|
|
116
|
-
function
|
|
163
|
+
function buildCloudRegions(yamlText, { clusterType } = {}) {
|
|
117
164
|
let data;
|
|
118
165
|
try {
|
|
119
166
|
data = jsYaml.load(yamlText);
|
|
@@ -146,13 +193,33 @@ function processCloudRegions(yamlText) {
|
|
|
146
193
|
} else {
|
|
147
194
|
console.warn('[cloud-regions] WARN: No products array found in YAML.');
|
|
148
195
|
}
|
|
196
|
+
// Accept any cluster type the source data actually offers, not only the ones
|
|
197
|
+
// clusterTypeMap knows about: an unmapped type such as CLUSTER_TYPE_SERVERLESS
|
|
198
|
+
// already shows up verbatim in the unfiltered table, so refusing to filter on
|
|
199
|
+
// it would be inconsistent with the output.
|
|
200
|
+
let clusterTypeFilter;
|
|
201
|
+
if (clusterType) {
|
|
202
|
+
const validClusterTypes = [...collectClusterTypes(data.regions, publicProductNames)].sort();
|
|
203
|
+
const wanted = String(clusterType).trim().toLowerCase();
|
|
204
|
+
clusterTypeFilter = validClusterTypes.find((ct) => ct.toLowerCase() === wanted);
|
|
205
|
+
if (!clusterTypeFilter) {
|
|
206
|
+
throw new Error(`Unsupported cluster type: ${clusterType}. Use one of: ${validClusterTypes.join(', ')}.`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
149
209
|
// Prepare providers array for template, only including public products and using name
|
|
150
210
|
const providers = providerOrder
|
|
151
211
|
.filter((prov) => grouped[prov] && grouped[prov].length > 0)
|
|
152
212
|
.map((prov) => {
|
|
153
213
|
// Only include regions that have at least one public product/tier
|
|
154
214
|
const filteredRegions = grouped[prov].map((region) => {
|
|
155
|
-
|
|
215
|
+
// Sort so a pure reordering upstream cannot produce a docs diff, and join
|
|
216
|
+
// with ', ' so long zone lists can wrap inside table cells.
|
|
217
|
+
const zones = Array.isArray(region.zones)
|
|
218
|
+
? [...region.zones]
|
|
219
|
+
.map(String)
|
|
220
|
+
.sort((a, b) => a.localeCompare(b, 'en', { numeric: true }))
|
|
221
|
+
.join(', ')
|
|
222
|
+
: (region.zones || '');
|
|
156
223
|
let tiers = [];
|
|
157
224
|
if (region.redpandaProductAvailability && typeof region.redpandaProductAvailability === 'object') {
|
|
158
225
|
// Group by tier name, collect all cluster types for that tier
|
|
@@ -161,14 +228,22 @@ function processCloudRegions(yamlText) {
|
|
|
161
228
|
if (!t.redpandaProductName || !publicProductNames.has(t.redpandaProductName)) {
|
|
162
229
|
continue;
|
|
163
230
|
}
|
|
231
|
+
let displayTypes = Array.isArray(t.clusterTypes) ? t.clusterTypes.map(displayClusterType) : [];
|
|
232
|
+
if (clusterTypeFilter) {
|
|
233
|
+
displayTypes = displayTypes.filter((ct) => ct === clusterTypeFilter);
|
|
234
|
+
if (displayTypes.length === 0) continue;
|
|
235
|
+
}
|
|
164
236
|
const productName = t.redpandaProductName;
|
|
165
237
|
if (!tierMap[productName]) tierMap[productName] = new Set();
|
|
166
|
-
|
|
167
|
-
for (const ct of t.clusterTypes) tierMap[productName].add(displayClusterType(ct));
|
|
168
|
-
}
|
|
238
|
+
for (const ct of displayTypes) tierMap[productName].add(ct);
|
|
169
239
|
}
|
|
170
240
|
tiers = Object.entries(tierMap)
|
|
171
|
-
|
|
241
|
+
// With a filter every tier carries exactly the filtered type, so
|
|
242
|
+
// repeating it on all 50-odd rows is noise: the template says it
|
|
243
|
+
// once from clusterType instead.
|
|
244
|
+
.map(([productName, cts]) => (clusterTypeFilter
|
|
245
|
+
? productName
|
|
246
|
+
: `${productName}: ${Array.from(cts).sort().join(', ')}`))
|
|
172
247
|
.sort((a, b) => a.localeCompare(b));
|
|
173
248
|
}
|
|
174
249
|
return {
|
|
@@ -178,10 +253,12 @@ function processCloudRegions(yamlText) {
|
|
|
178
253
|
};
|
|
179
254
|
}).filter(region => region.tiers && region.tiers.length > 0);
|
|
180
255
|
if (filteredRegions.length === 0) {
|
|
181
|
-
|
|
256
|
+
const because = clusterTypeFilter ? ` available for ${clusterTypeFilter} clusters` : '';
|
|
257
|
+
console.info(`[cloud-regions] INFO: No public tiers${because} found for provider '${prov}'.`);
|
|
182
258
|
}
|
|
183
259
|
return {
|
|
184
260
|
name: prov,
|
|
261
|
+
displayName: providerDisplayNames[prov] || prov,
|
|
185
262
|
regions: filteredRegions,
|
|
186
263
|
};
|
|
187
264
|
})
|
|
@@ -189,7 +266,22 @@ function processCloudRegions(yamlText) {
|
|
|
189
266
|
if (providers.length === 0) {
|
|
190
267
|
console.warn('[cloud-regions] WARN: No providers/regions found after filtering.');
|
|
191
268
|
}
|
|
192
|
-
return providers;
|
|
269
|
+
return { providers, clusterType: clusterTypeFilter };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Parses YAML text describing cloud regions and products, filters for public products, and organizes regions by provider.
|
|
274
|
+
*
|
|
275
|
+
* 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.
|
|
276
|
+
*
|
|
277
|
+
* @param {string} yamlText - The YAML content to parse and process.
|
|
278
|
+
* @param {Object} [options] - Processing options.
|
|
279
|
+
* @param {string} [options.clusterType] - Optional cluster type display name ('BYOC' or 'Dedicated', case-insensitive). When set, only tiers available for that cluster type are included, and regions with no matching tiers are dropped.
|
|
280
|
+
* @return {Array<Object>} An array of provider objects, each containing a name and a list of regions with their available public product tiers.
|
|
281
|
+
* @throws {Error} If the YAML is malformed or missing the required `regions` array.
|
|
282
|
+
*/
|
|
283
|
+
function processCloudRegions(yamlText, options = {}) {
|
|
284
|
+
return buildCloudRegions(yamlText, options).providers;
|
|
193
285
|
}
|
|
194
286
|
|
|
195
287
|
/**
|
|
@@ -206,10 +298,11 @@ function processCloudRegions(yamlText) {
|
|
|
206
298
|
* @param {string} [options.format='md'] - The output format (for example, 'md' for Markdown).
|
|
207
299
|
* @param {string} [options.token] - Optional GitHub token for authentication.
|
|
208
300
|
* @param {string} [options.template] - Optional path to custom Handlebars template.
|
|
301
|
+
* @param {string} [options.clusterType] - Optional cluster type filter ('BYOC' or 'Dedicated', case-insensitive).
|
|
209
302
|
* @returns {string} The rendered cloud regions output.
|
|
210
303
|
* @throws {Error} If fetching, processing, or rendering fails, or if no valid providers or regions are found.
|
|
211
304
|
*/
|
|
212
|
-
async function generateCloudRegions({ owner, repo, path, ref = 'main', format = 'md', token, template }) {
|
|
305
|
+
async function generateCloudRegions({ owner, repo, path, ref = 'main', format = 'md', token, template, clusterType }) {
|
|
213
306
|
let yamlText;
|
|
214
307
|
try {
|
|
215
308
|
yamlText = await fetchYaml({ owner, repo, path, ref, token });
|
|
@@ -218,8 +311,9 @@ async function generateCloudRegions({ owner, repo, path, ref = 'main', format =
|
|
|
218
311
|
throw err;
|
|
219
312
|
}
|
|
220
313
|
let providers;
|
|
314
|
+
let resolvedClusterType;
|
|
221
315
|
try {
|
|
222
|
-
providers =
|
|
316
|
+
({ providers, clusterType: resolvedClusterType } = buildCloudRegions(yamlText, { clusterType }));
|
|
223
317
|
} catch (err) {
|
|
224
318
|
console.error(`[cloud-regions] ERROR: Failed to process cloud regions: ${err.message}`);
|
|
225
319
|
throw err;
|
|
@@ -230,7 +324,7 @@ async function generateCloudRegions({ owner, repo, path, ref = 'main', format =
|
|
|
230
324
|
}
|
|
231
325
|
const lastUpdated = new Date().toISOString();
|
|
232
326
|
try {
|
|
233
|
-
return renderCloudRegions({ providers, format, lastUpdated, template });
|
|
327
|
+
return renderCloudRegions({ providers, format, lastUpdated, template, clusterType: resolvedClusterType });
|
|
234
328
|
} catch (err) {
|
|
235
329
|
console.error(`[cloud-regions] ERROR: Failed to render cloud regions: ${err.message}`);
|
|
236
330
|
throw err;
|
|
@@ -12,10 +12,12 @@ const handlebars = require('handlebars');
|
|
|
12
12
|
* @param {Array} opts.providers - List of cloud provider objects, each with a name and an array of regions.
|
|
13
13
|
* @param {string} opts.format - Output format, either 'md' (Markdown) or 'adoc' (AsciiDoc).
|
|
14
14
|
* @param {string} [opts.lastUpdated] - Optional ISO timestamp indicating when the data was last updated.
|
|
15
|
+
* @param {string} [opts.template] - Optional absolute path to a custom Handlebars template. Overrides the bundled template for the given format. The caller is responsible for containing this path: bin/doc-tools.js resolves it with resolveInsideRepo before calling, and that is the single enforcement point for the CLI and the MCP server alike.
|
|
16
|
+
* @param {string} [opts.clusterType] - Optional cluster type the data was filtered to. Exposed to the template so a filtered table can name it.
|
|
15
17
|
* @returns {string} The rendered output string.
|
|
16
18
|
* @throws {Error} If the providers array is missing or empty.
|
|
17
19
|
*/
|
|
18
|
-
function renderCloudRegions({ providers, format, lastUpdated }) {
|
|
20
|
+
function renderCloudRegions({ providers, format, lastUpdated, template, clusterType }) {
|
|
19
21
|
if (!Array.isArray(providers) || providers.length === 0) {
|
|
20
22
|
throw new Error('No providers/regions found in YAML.');
|
|
21
23
|
}
|
|
@@ -27,19 +29,23 @@ function renderCloudRegions({ providers, format, lastUpdated }) {
|
|
|
27
29
|
...provider,
|
|
28
30
|
regions: [...provider.regions].sort((a, b) => a.name.localeCompare(b.name))
|
|
29
31
|
}));
|
|
30
|
-
const templateFile = path.join(__dirname, `cloud-regions-table-${format}.hbs`);
|
|
32
|
+
const templateFile = template || path.join(__dirname, `cloud-regions-table-${format}.hbs`);
|
|
31
33
|
if (!fs.existsSync(templateFile)) {
|
|
32
34
|
throw new Error(`Template file not found: ${templateFile}`);
|
|
33
35
|
}
|
|
34
|
-
let
|
|
36
|
+
let compiledTemplate;
|
|
35
37
|
try {
|
|
36
|
-
templateSrc = fs.readFileSync(templateFile, 'utf8');
|
|
37
|
-
|
|
38
|
+
const templateSrc = fs.readFileSync(templateFile, 'utf8');
|
|
39
|
+
// compile() only parses on first render, so a syntax error in a custom
|
|
40
|
+
// template would otherwise be reported as a render failure and send the
|
|
41
|
+
// author looking at their data instead of their braces. precompile parses now.
|
|
42
|
+
handlebars.precompile(templateSrc);
|
|
43
|
+
compiledTemplate = handlebars.compile(templateSrc);
|
|
38
44
|
} catch (err) {
|
|
39
45
|
throw new Error(`Failed to compile Handlebars template at ${templateFile}: ${err.message}`);
|
|
40
46
|
}
|
|
41
47
|
try {
|
|
42
|
-
return
|
|
48
|
+
return compiledTemplate({ providers: sortedProviders, lastUpdated, clusterType });
|
|
43
49
|
} catch (err) {
|
|
44
50
|
throw new Error(`Failed to render Handlebars template at ${templateFile}: ${err.message}`);
|
|
45
51
|
}
|