@redpanda-data/docs-extensions-and-macros 5.10.0 → 5.11.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 +94 -1
- package/cli-utils/helm-commented-values.js +361 -0
- package/package.json +1 -1
- package/tools/generate-cli-docs.js +1 -0
- package/tools/rpk-docs/generate-rpk-docs.js +62 -0
- package/tools/rpk-docs/generate-x-env-partial.js +284 -0
- package/tools/rpk-docs/rpk-docs-handler.js +11 -21
package/bin/doc-tools.js
CHANGED
|
@@ -1279,6 +1279,56 @@ automation
|
|
|
1279
1279
|
}
|
|
1280
1280
|
})
|
|
1281
1281
|
|
|
1282
|
+
/**
|
|
1283
|
+
* generate rpk-env-partial
|
|
1284
|
+
*
|
|
1285
|
+
* @description
|
|
1286
|
+
* Generates the -X option -> RPK_* environment variable mapping table as an
|
|
1287
|
+
* AsciiDoc partial from rpk's own -X option data, so the table cannot drift
|
|
1288
|
+
* from the CLI. Prefers the structured x_options array in `rpk --print-tree`
|
|
1289
|
+
* (which carries the env var names rpk itself derives) and falls back to
|
|
1290
|
+
* parsing `-X list` text for rpk versions that predate it. Hidden -X options
|
|
1291
|
+
* appear in neither source, so they are excluded automatically.
|
|
1292
|
+
*
|
|
1293
|
+
* The main rpk-docs pipeline also writes this partial from the tree it
|
|
1294
|
+
* already holds; this standalone command is for targeted refreshes without
|
|
1295
|
+
* a full generation run.
|
|
1296
|
+
*
|
|
1297
|
+
* @example
|
|
1298
|
+
* # Generate for a release tag (sparse-clones redpanda, builds rpk with Go)
|
|
1299
|
+
* npx doc-tools generate rpk-env-partial --ref v26.2.1 --output modules/reference/partials/rpk-env-vars.adoc
|
|
1300
|
+
*
|
|
1301
|
+
* # Use a local source checkout (as-is, no checkout changes), an existing
|
|
1302
|
+
* # rpk binary, or a versioned tree snapshot (no clone or build at all)
|
|
1303
|
+
* npx doc-tools generate rpk-env-partial --from-source ~/redpanda --output modules/reference/partials/rpk-env-vars.adoc
|
|
1304
|
+
* npx doc-tools generate rpk-env-partial --rpk-bin "$(command -v rpk)" --output modules/reference/partials/rpk-env-vars.adoc
|
|
1305
|
+
* npx doc-tools generate rpk-env-partial --from-json docs-data/rpk-v26.2.1.json --output modules/reference/partials/rpk-env-vars.adoc
|
|
1306
|
+
*/
|
|
1307
|
+
automation
|
|
1308
|
+
.command('rpk-env-partial')
|
|
1309
|
+
.description('Generate the -X -> RPK_* env var mapping partial from rpk -X list output.')
|
|
1310
|
+
.option('-r, --ref <ref>', 'Git branch or tag to build rpk from (e.g., dev, v26.2.1). Clones from GitHub.')
|
|
1311
|
+
.option('--from-source <path>', 'Path to local rpk source (src/go/rpk directory)')
|
|
1312
|
+
.option('--rpk-bin <path>', 'Path to an existing rpk binary (skips clone and build)')
|
|
1313
|
+
.option('--from-json <path>', 'Versioned tree snapshot from docs-data (skips clone and build; requires a snapshot with x_options)')
|
|
1314
|
+
.option('--output <path>', 'Path to write the partial to', 'modules/reference/partials/rpk-env-vars.adoc')
|
|
1315
|
+
.action((options) => {
|
|
1316
|
+
try {
|
|
1317
|
+
const { handleXEnvPartialGeneration } = require('../tools/rpk-docs/generate-x-env-partial.js')
|
|
1318
|
+
const result = handleXEnvPartialGeneration({
|
|
1319
|
+
ref: options.ref,
|
|
1320
|
+
fromSource: options.fromSource,
|
|
1321
|
+
rpkBin: options.rpkBin,
|
|
1322
|
+
fromJson: options.fromJson,
|
|
1323
|
+
output: options.output
|
|
1324
|
+
})
|
|
1325
|
+
console.log(`Wrote ${result.keyCount} -X option mappings to ${result.output} (source: ${result.source})`)
|
|
1326
|
+
} catch (err) {
|
|
1327
|
+
console.error(`Error: ${err.message}`)
|
|
1328
|
+
process.exit(1)
|
|
1329
|
+
}
|
|
1330
|
+
})
|
|
1331
|
+
|
|
1282
1332
|
/**
|
|
1283
1333
|
* generate rpk-plugin-stubs
|
|
1284
1334
|
*
|
|
@@ -1677,7 +1727,50 @@ automation
|
|
|
1677
1727
|
if (r.status !== 0) process.exit(r.status)
|
|
1678
1728
|
|
|
1679
1729
|
const { formatHelmSpec } = require('../cli-utils/format-helm-spec')
|
|
1680
|
-
|
|
1730
|
+
let doc = formatHelmSpec(fs.readFileSync(outFile, 'utf8'))
|
|
1731
|
+
|
|
1732
|
+
// helm-docs only renders keys that exist in the YAML tree, so optional
|
|
1733
|
+
// values that ship commented out never appear in the reference. Pull
|
|
1734
|
+
// documented commented-out keys from values.yaml and inject them. This
|
|
1735
|
+
// runs after formatHelmSpec so the injected body sections are not affected
|
|
1736
|
+
// by the blank-line normalization that its header fix performs.
|
|
1737
|
+
const valuesFile = path.join(chartPath, 'values.yaml')
|
|
1738
|
+
if (fs.existsSync(valuesFile)) {
|
|
1739
|
+
const { extractCommentedValueDocs, injectIntoAsciiDoc, filterEntriesBySchema } = require('../cli-utils/helm-commented-values')
|
|
1740
|
+
try {
|
|
1741
|
+
let entries = extractCommentedValueDocs(fs.readFileSync(valuesFile, 'utf8'))
|
|
1742
|
+
|
|
1743
|
+
// Deprecation notices and other prose can produce paths the chart
|
|
1744
|
+
// no longer accepts (for example the removed
|
|
1745
|
+
// storage.tiered.credentialsSecretRef.configurationKey). When the
|
|
1746
|
+
// chart ships a values schema, drop any path it provably rejects.
|
|
1747
|
+
const schemaFile = path.join(chartPath, 'values.schema.json')
|
|
1748
|
+
if (entries.length > 0 && fs.existsSync(schemaFile)) {
|
|
1749
|
+
const schema = JSON.parse(fs.readFileSync(schemaFile, 'utf8'))
|
|
1750
|
+
const { accepted, rejected } = filterEntriesBySchema(entries, schema)
|
|
1751
|
+
if (rejected.length > 0) {
|
|
1752
|
+
console.warn(`Warning: Skipping ${rejected.length} commented-out value(s) rejected by values.schema.json: ${rejected.map((e) => e.path).join(', ')}`)
|
|
1753
|
+
}
|
|
1754
|
+
entries = accepted
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
if (entries.length > 0) {
|
|
1758
|
+
const { doc: withInjected, injected, sectionsFound } = injectIntoAsciiDoc(doc, entries)
|
|
1759
|
+
doc = withInjected
|
|
1760
|
+
if (injected.length > 0) {
|
|
1761
|
+
console.log(`Documented ${injected.length} commented-out value(s): ${injected.join(', ')}`)
|
|
1762
|
+
} else if (sectionsFound === 0) {
|
|
1763
|
+
// Without this, a heading-level change upstream would silently
|
|
1764
|
+
// drop every documented commented-out value from the reference.
|
|
1765
|
+
console.warn(`Warning: No value section headings found in ${name} to anchor commented-out value docs; dropped: ${entries.map((e) => e.path).join(', ')}`)
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
} catch (err) {
|
|
1769
|
+
console.warn(`Warning: Skipping commented-out value docs for ${name}: ${err.message}`)
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
fs.writeFileSync(outFile, doc, 'utf8')
|
|
1681
1774
|
|
|
1682
1775
|
console.log(`Done: Wrote ${outFile}`)
|
|
1683
1776
|
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extracts documentation for commented-out values in a Helm chart's
|
|
5
|
+
* values.yaml, and injects the resulting entries into the AsciiDoc reference
|
|
6
|
+
* that `doc-tools generate helm-spec` produces.
|
|
7
|
+
*
|
|
8
|
+
* helm-docs only renders rows for keys that exist in the YAML tree, so
|
|
9
|
+
* optional values that ship commented out (for example `external.domain` in
|
|
10
|
+
* the Redpanda chart) never appear in the generated reference, even when the
|
|
11
|
+
* chart author wrote a `# --` description for them. This module closes that
|
|
12
|
+
* gap without requiring any change to chart behavior.
|
|
13
|
+
*
|
|
14
|
+
* Two comment conventions are recognized:
|
|
15
|
+
*
|
|
16
|
+
* 1. helm-docs style, attached to a commented-out key. A comment block that
|
|
17
|
+
* contains a `# -- description` marker and is immediately followed by a
|
|
18
|
+
* commented-out key documents that key. The key's path is derived from
|
|
19
|
+
* the enclosing uncommented keys by indentation:
|
|
20
|
+
*
|
|
21
|
+
* external:
|
|
22
|
+
* # -- Optional domain advertised to external clients.
|
|
23
|
+
* # domain: local
|
|
24
|
+
*
|
|
25
|
+
* yields `external.domain`.
|
|
26
|
+
*
|
|
27
|
+
* 2. Explicit, fully-qualified syntax that works anywhere in the file:
|
|
28
|
+
*
|
|
29
|
+
* # @doc external.addresses -- Optional list of advertised addresses.
|
|
30
|
+
* # @default -- `nil`
|
|
31
|
+
*
|
|
32
|
+
* In both forms, an optional `# @default -- text` line overrides the
|
|
33
|
+
* displayed default, which is otherwise `nil`.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
// Key names never contain dots or slashes: a dotted or slashed "key" in a
|
|
37
|
+
// comment is prose or a URL (`# docker.redpanda.com/...:4.101.0`), and
|
|
38
|
+
// treating it as a key both fabricates a path and truncates the description.
|
|
39
|
+
// Fully-qualified dotted paths are supported through the @doc syntax instead.
|
|
40
|
+
const COMMENTED_KEY_RE = /^(\s*)#(\s{0,4})([a-z][A-Za-z0-9_-]*):\s*(.*)$/
|
|
41
|
+
const REAL_KEY_RE = /^(\s*)([A-Za-z0-9_."/-]+):(.*)$/
|
|
42
|
+
// `(?!-)` keeps comment dividers such as `# ----------------` from being
|
|
43
|
+
// mistaken for an empty description marker.
|
|
44
|
+
const DESC_MARKER_RE = /^\s*#\s*--\s*(?!-)(.*)$/
|
|
45
|
+
const AT_DOC_RE = /^\s*#\s*@doc\s+([A-Za-z0-9_./-]+)\s+--\s*(.*)$/
|
|
46
|
+
const AT_DEFAULT_RE = /^\s*#\s*@default\s+--\s*(.*)$/
|
|
47
|
+
const COMMENT_RE = /^\s*#\s?(.*)$/
|
|
48
|
+
// A block scalar header may carry an explicit indentation indicator digit on
|
|
49
|
+
// either side of the chomping indicator: `|2`, `|-2`, `|2-`, `>2`, ...
|
|
50
|
+
const BLOCK_SCALAR_RE = /[|>][0-9]?[+-]?[0-9]?\s*(#.*)?$/
|
|
51
|
+
const LIST_ITEM_RE = /^\s*-\s/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parse values.yaml text and return entries for documented commented-out keys.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} yamlText - Raw contents of values.yaml.
|
|
57
|
+
* @returns {Array<{path: string, description: string, default: string}>}
|
|
58
|
+
*/
|
|
59
|
+
function extractCommentedValueDocs (yamlText) {
|
|
60
|
+
const lines = yamlText.split(/\r?\n/)
|
|
61
|
+
const entries = []
|
|
62
|
+
const stack = [] // enclosing real keys: { name, indent }
|
|
63
|
+
|
|
64
|
+
// Accumulating comment block: { descLines, default, candidate }. The
|
|
65
|
+
// candidate is the key the block documents; emission is deferred to the
|
|
66
|
+
// end of the block so that a later key line at the same depth supersedes a
|
|
67
|
+
// prose line that merely looks like a key (`# example:`, `# default: 30s`).
|
|
68
|
+
let block = null
|
|
69
|
+
let atDoc = null // accumulating @doc entry: { path, descLines, default }
|
|
70
|
+
let skipScalarIndent = -1 // inside a block scalar when >= 0
|
|
71
|
+
let suppressInnerIndent = -1 // inside an emitted commented key's subtree when >= 0
|
|
72
|
+
|
|
73
|
+
const flushAtDoc = () => {
|
|
74
|
+
if (atDoc) {
|
|
75
|
+
entries.push({
|
|
76
|
+
path: atDoc.path,
|
|
77
|
+
description: atDoc.descLines.join('\n').trim(),
|
|
78
|
+
default: atDoc.default || '`nil`',
|
|
79
|
+
})
|
|
80
|
+
atDoc = null
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Emit the active block's candidate, if any, and close the block. The
|
|
85
|
+
// emitted key suppresses its own commented subtree until real content
|
|
86
|
+
// appears, so nested example structures do not emit bogus paths.
|
|
87
|
+
const flushBlock = () => {
|
|
88
|
+
if (block && block.candidate) {
|
|
89
|
+
const { name, effIndent } = block.candidate
|
|
90
|
+
const parents = stack.filter((k) => k.indent < effIndent).map((k) => k.name)
|
|
91
|
+
entries.push({
|
|
92
|
+
path: [...parents, name].join('.'),
|
|
93
|
+
description: block.descLines.join('\n').trim(),
|
|
94
|
+
default: block.default || '`nil`',
|
|
95
|
+
})
|
|
96
|
+
suppressInnerIndent = effIndent
|
|
97
|
+
}
|
|
98
|
+
block = null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const line of lines) {
|
|
102
|
+
// Skip the body of block scalars so their content is never mistaken for
|
|
103
|
+
// keys or comments.
|
|
104
|
+
if (skipScalarIndent >= 0) {
|
|
105
|
+
const indent = line.match(/^\s*/)[0].length
|
|
106
|
+
if (line.trim() === '' || indent > skipScalarIndent) continue
|
|
107
|
+
skipScalarIndent = -1
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const atDocMatch = line.match(AT_DOC_RE)
|
|
111
|
+
if (atDocMatch) {
|
|
112
|
+
flushAtDoc()
|
|
113
|
+
flushBlock()
|
|
114
|
+
atDoc = { path: atDocMatch[1], descLines: [atDocMatch[2]], default: '' }
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const atDefault = line.match(AT_DEFAULT_RE)
|
|
119
|
+
if (atDefault) {
|
|
120
|
+
if (atDoc) atDoc.default = atDefault[1].trim()
|
|
121
|
+
else if (block) block.default = atDefault[1].trim()
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const commentedKey = line.match(COMMENTED_KEY_RE)
|
|
126
|
+
if (commentedKey) {
|
|
127
|
+
// A commented-out key line terminates an @doc entry rather than being
|
|
128
|
+
// swallowed into its description, so @doc lines can sit directly above
|
|
129
|
+
// the commented key they document.
|
|
130
|
+
flushAtDoc()
|
|
131
|
+
// Authors indent a commented key either before the marker (" # child:")
|
|
132
|
+
// or after it ("# child:"), and the two forms mean the same nesting.
|
|
133
|
+
// Counting only one side made them disagree: a child indented before the
|
|
134
|
+
// marker escaped its parent's subtree and was emitted as a top-level
|
|
135
|
+
// path. Fold both sides into one depth, treating the single space that
|
|
136
|
+
// separates "#" from its text as punctuation rather than indentation, so
|
|
137
|
+
// the result lines up with the real-key indents held on the stack.
|
|
138
|
+
const effIndent = commentedKey[1].length + Math.max(0, commentedKey[2].length - 1)
|
|
139
|
+
// A previously emitted commented key suppresses its own commented
|
|
140
|
+
// subtree, so nested example structures do not emit bogus paths.
|
|
141
|
+
if (suppressInnerIndent >= 0 && effIndent > suppressInnerIndent) {
|
|
142
|
+
block = null
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
suppressInnerIndent = -1
|
|
146
|
+
// Only bare keys or simple scalar values look like real chart values.
|
|
147
|
+
// Prose such as `# Warning: If you use LoadBalancers...` has spaces in
|
|
148
|
+
// the trailing text and is rejected.
|
|
149
|
+
const value = commentedKey[4].trim()
|
|
150
|
+
// Reject URL-like lines such as `# https://github.com/...`, where the
|
|
151
|
+
// "key" is a URL scheme and the "value" is the scheme-relative part.
|
|
152
|
+
const simpleValue = (value === '' || !/\s/.test(value)) && !value.startsWith('/')
|
|
153
|
+
if (block && simpleValue) {
|
|
154
|
+
if (!block.candidate || effIndent <= block.candidate.effIndent) {
|
|
155
|
+
// The last key-shaped line at the block's shallowest depth wins:
|
|
156
|
+
// an earlier line at the same depth was prose shaped like a key,
|
|
157
|
+
// and deeper lines are nested example structures.
|
|
158
|
+
block.candidate = { name: commentedKey[3], effIndent }
|
|
159
|
+
}
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const descMarker = line.match(DESC_MARKER_RE)
|
|
165
|
+
if (descMarker) {
|
|
166
|
+
flushAtDoc()
|
|
167
|
+
flushBlock()
|
|
168
|
+
block = { descLines: [descMarker[1]], default: '', candidate: null }
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const comment = line.match(COMMENT_RE)
|
|
173
|
+
if (comment && line.trim().startsWith('#')) {
|
|
174
|
+
if (atDoc) atDoc.descLines.push(comment[1])
|
|
175
|
+
// Description lines precede the documented key; once a candidate
|
|
176
|
+
// exists, trailing comment lines belong to its example structure.
|
|
177
|
+
else if (block && !block.candidate) block.descLines.push(comment[1])
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Any non-comment line ends comment accumulation. Only real content also
|
|
182
|
+
// ends subtree suppression: a blank line inside a commented-out example
|
|
183
|
+
// structure must not re-enable emission against a stale parent key, since
|
|
184
|
+
// the real-key stack is not rebuilt by comment lines.
|
|
185
|
+
flushAtDoc()
|
|
186
|
+
flushBlock()
|
|
187
|
+
if (line.trim() === '') continue
|
|
188
|
+
suppressInnerIndent = -1
|
|
189
|
+
|
|
190
|
+
if (LIST_ITEM_RE.test(line)) continue
|
|
191
|
+
|
|
192
|
+
const realKey = line.match(REAL_KEY_RE)
|
|
193
|
+
if (realKey) {
|
|
194
|
+
const indent = realKey[1].length
|
|
195
|
+
while (stack.length > 0 && stack[stack.length - 1].indent >= indent) {
|
|
196
|
+
stack.pop()
|
|
197
|
+
}
|
|
198
|
+
stack.push({ name: realKey[2].replace(/^"|"$/g, ''), indent })
|
|
199
|
+
if (BLOCK_SCALAR_RE.test(realKey[3])) skipScalarIndent = indent
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
flushAtDoc()
|
|
204
|
+
flushBlock()
|
|
205
|
+
return entries
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Check a dotted value path against a chart's values.schema.json. A path is
|
|
210
|
+
* rejected only when the walk can prove the schema forbids it: an object
|
|
211
|
+
* level whose `properties` lack the segment, whose `patternProperties` (if
|
|
212
|
+
* any) do not match it, and whose `additionalProperties` is `false`.
|
|
213
|
+
* Anything the walk cannot interpret ($ref, allOf, open objects) is allowed,
|
|
214
|
+
* so an incomplete schema never hides documentation.
|
|
215
|
+
*
|
|
216
|
+
* @param {object} schema - Parsed values.schema.json.
|
|
217
|
+
* @param {string} path - Dotted value path, for example `external.domain`.
|
|
218
|
+
* @returns {boolean}
|
|
219
|
+
*/
|
|
220
|
+
function isPathAllowedBySchema (schema, path) {
|
|
221
|
+
let node = schema
|
|
222
|
+
for (const segment of path.split('.')) {
|
|
223
|
+
if (!node || typeof node !== 'object' || node.$ref || node.allOf || node.anyOf || node.oneOf) {
|
|
224
|
+
return true
|
|
225
|
+
}
|
|
226
|
+
const props = node.properties
|
|
227
|
+
if (props && Object.prototype.hasOwnProperty.call(props, segment)) {
|
|
228
|
+
node = props[segment]
|
|
229
|
+
continue
|
|
230
|
+
}
|
|
231
|
+
if (node.patternProperties) {
|
|
232
|
+
const pattern = Object.keys(node.patternProperties)
|
|
233
|
+
.find((p) => { try { return new RegExp(p).test(segment) } catch { return true } })
|
|
234
|
+
if (pattern !== undefined) {
|
|
235
|
+
node = node.patternProperties[pattern]
|
|
236
|
+
continue
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return node.additionalProperties !== false
|
|
240
|
+
}
|
|
241
|
+
return true
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Split extracted entries into those permitted by the chart's
|
|
246
|
+
* values.schema.json and those provably rejected by it, such as keys whose
|
|
247
|
+
* `# --` comment is a deprecation notice for a removed value.
|
|
248
|
+
*
|
|
249
|
+
* @param {Array<{path: string}>} entries
|
|
250
|
+
* @param {object} schema - Parsed values.schema.json.
|
|
251
|
+
* @returns {{accepted: Array, rejected: Array}}
|
|
252
|
+
*/
|
|
253
|
+
function filterEntriesBySchema (entries, schema) {
|
|
254
|
+
const accepted = []
|
|
255
|
+
const rejected = []
|
|
256
|
+
for (const entry of entries) {
|
|
257
|
+
(isPathAllowedBySchema(schema, entry.path) ? accepted : rejected).push(entry)
|
|
258
|
+
}
|
|
259
|
+
return { accepted, rejected }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Neutralize description lines that would otherwise parse as AsciiDoc
|
|
263
|
+
// structure when injected at block level: section titles re-parent the
|
|
264
|
+
// following sections, and an unclosed block delimiter swallows the rest of
|
|
265
|
+
// the document. `{empty}` renders as nothing but demotes the line to a
|
|
266
|
+
// plain paragraph.
|
|
267
|
+
function sanitizeDescription (text) {
|
|
268
|
+
return text
|
|
269
|
+
.split('\n')
|
|
270
|
+
.map((line) => (/^(=+\s|-{2,}\s*$|[=*_.+]{4,}\s*$|\|===)/.test(line) ? `{empty}${line}` : line))
|
|
271
|
+
.join('\n')
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Inject extracted entries into a generated AsciiDoc Helm reference.
|
|
276
|
+
*
|
|
277
|
+
* Sections are matched by their `=== link:++<url>?...path=<key>++[<key>]`
|
|
278
|
+
* headings. New sections reuse the Artifact Hub URL prefix of the existing
|
|
279
|
+
* sections and are inserted in alphabetical key order. Keys that are already
|
|
280
|
+
* documented are skipped.
|
|
281
|
+
*
|
|
282
|
+
* @param {string} adoc - The converted AsciiDoc document.
|
|
283
|
+
* @param {Array<{path: string, description: string, default: string}>} entries
|
|
284
|
+
* @returns {{doc: string, injected: string[], sectionsFound: number}}
|
|
285
|
+
*/
|
|
286
|
+
function injectIntoAsciiDoc (adoc, entries) {
|
|
287
|
+
// The label may itself contain brackets (`[storage.volume[0].name]` in the
|
|
288
|
+
// connectors chart), so it is matched to end of line rather than to the
|
|
289
|
+
// first closing bracket.
|
|
290
|
+
const headingRe = /^=== link:\+\+(https?:\/\/[^+]*[?&]path=)([^+]+)\+\+\[.*\]\s*$/gm
|
|
291
|
+
const sections = []
|
|
292
|
+
let match
|
|
293
|
+
while ((match = headingRe.exec(adoc)) !== null) {
|
|
294
|
+
sections.push({ index: match.index, urlPrefix: match[1], key: match[2] })
|
|
295
|
+
}
|
|
296
|
+
if (sections.length === 0) {
|
|
297
|
+
return { doc: adoc, injected: [], sectionsFound: 0 }
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const urlPrefix = sections[0].urlPrefix
|
|
301
|
+
// Tracks both keys present in the document and keys queued for injection,
|
|
302
|
+
// so a path extracted twice (for example, from both @doc and # -- comments
|
|
303
|
+
// for the same key) is only injected once.
|
|
304
|
+
const existingKeys = new Set(sections.map((s) => s.key))
|
|
305
|
+
const additions = []
|
|
306
|
+
for (const entry of entries) {
|
|
307
|
+
if (existingKeys.has(entry.path)) continue
|
|
308
|
+
existingKeys.add(entry.path)
|
|
309
|
+
additions.push(entry)
|
|
310
|
+
}
|
|
311
|
+
additions.sort((a, b) => a.path.localeCompare(b.path))
|
|
312
|
+
|
|
313
|
+
let doc = adoc
|
|
314
|
+
const injected = []
|
|
315
|
+
|
|
316
|
+
for (const entry of additions) {
|
|
317
|
+
// Recompute section offsets after each insertion.
|
|
318
|
+
const current = []
|
|
319
|
+
headingRe.lastIndex = 0
|
|
320
|
+
while ((match = headingRe.exec(doc)) !== null) {
|
|
321
|
+
current.push({ index: match.index, key: match[2] })
|
|
322
|
+
}
|
|
323
|
+
const after = current.find((s) => s.key.localeCompare(entry.path) > 0)
|
|
324
|
+
|
|
325
|
+
const section = [
|
|
326
|
+
`=== link:++${urlPrefix}${entry.path}++[${entry.path}]`,
|
|
327
|
+
'',
|
|
328
|
+
sanitizeDescription(entry.description),
|
|
329
|
+
'',
|
|
330
|
+
`*Default:* ${entry.default}`,
|
|
331
|
+
'',
|
|
332
|
+
'',
|
|
333
|
+
].join('\n')
|
|
334
|
+
|
|
335
|
+
if (after) {
|
|
336
|
+
doc = doc.slice(0, after.index) + section + doc.slice(after.index)
|
|
337
|
+
} else {
|
|
338
|
+
// Append after the final section, before the next heading (levels 1-3)
|
|
339
|
+
// if one exists, so a trailing non-values subsection stays last.
|
|
340
|
+
const last = current[current.length - 1]
|
|
341
|
+
const rest = doc.slice(last.index)
|
|
342
|
+
const nextHeading = rest.search(/\n={1,3} /)
|
|
343
|
+
if (nextHeading > 0) {
|
|
344
|
+
const cut = last.index + nextHeading + 1
|
|
345
|
+
doc = doc.slice(0, cut) + section + doc.slice(cut)
|
|
346
|
+
} else {
|
|
347
|
+
doc = doc.trimEnd() + '\n\n' + section
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
injected.push(entry.path)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return { doc, injected, sectionsFound: sections.length }
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
module.exports = {
|
|
357
|
+
extractCommentedValueDocs,
|
|
358
|
+
injectIntoAsciiDoc,
|
|
359
|
+
filterEntriesBySchema,
|
|
360
|
+
isPathAllowedBySchema,
|
|
361
|
+
}
|
package/package.json
CHANGED
|
@@ -2588,6 +2588,10 @@ async function generateRpkDocs(options = {}) {
|
|
|
2588
2588
|
overrides,
|
|
2589
2589
|
outputDir,
|
|
2590
2590
|
cloudSecretDir, // Directory for rpk cloud and rpk security secret commands
|
|
2591
|
+
// Directory for the shared -X -> RPK_* env vars partial. The handler passes
|
|
2592
|
+
// the partials dir it already derived; when absent (direct callers, tests)
|
|
2593
|
+
// it is derived from outputDir the same way.
|
|
2594
|
+
envPartialDir,
|
|
2591
2595
|
rpkVersion,
|
|
2592
2596
|
pluginVersions = {},
|
|
2593
2597
|
draftMissing = false,
|
|
@@ -3155,6 +3159,21 @@ async function generateRpkDocs(options = {}) {
|
|
|
3155
3159
|
navResult = updateNavFile(navFile, commands, resolvedOverrides, topLevelWithSubcommands, effectiveProtectedPlugins)
|
|
3156
3160
|
}
|
|
3157
3161
|
|
|
3162
|
+
// Regenerate the shared -X -> RPK_* env vars partial from the same tree,
|
|
3163
|
+
// so it always matches the rpk build the command pages came from. Trees
|
|
3164
|
+
// from rpk versions that predate x_options (redpanda#31520) skip with a
|
|
3165
|
+
// note; `doc-tools generate rpk-env-partial` covers those via its
|
|
3166
|
+
// -X list fallback.
|
|
3167
|
+
const envPartialResult = writeEnvVarsPartial(
|
|
3168
|
+
tree,
|
|
3169
|
+
envPartialDir || derivePartialsDir(outputDir)
|
|
3170
|
+
)
|
|
3171
|
+
if (envPartialResult.written) {
|
|
3172
|
+
console.log(` Regenerated env vars partial (${envPartialResult.keyCount} -X options): ${envPartialResult.output}`)
|
|
3173
|
+
} else {
|
|
3174
|
+
console.log(' Env vars partial not regenerated: tree has no x_options (rpk predates redpanda#31520)')
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3158
3177
|
return {
|
|
3159
3178
|
commandCount: commands.length,
|
|
3160
3179
|
filesGenerated,
|
|
@@ -3162,12 +3181,55 @@ async function generateRpkDocs(options = {}) {
|
|
|
3162
3181
|
filesFailed,
|
|
3163
3182
|
filesDeleted,
|
|
3164
3183
|
subdirectoriesCreated: createdSubdirs.size,
|
|
3184
|
+
envPartial: envPartialResult,
|
|
3165
3185
|
...navResult
|
|
3166
3186
|
}
|
|
3167
3187
|
}
|
|
3168
3188
|
|
|
3189
|
+
/**
|
|
3190
|
+
* Resolve the partials directory of the Antora module that owns the rpk pages.
|
|
3191
|
+
*
|
|
3192
|
+
* For the docs layout (`.../modules/reference/pages/rpk`) this is the sibling
|
|
3193
|
+
* of `pages`, so `.../modules/reference/partials`. For an arbitrary
|
|
3194
|
+
* `--output-dir` there is no `pages` segment to anchor on, so fall back to a
|
|
3195
|
+
* `partials` directory beside the output. Walking a fixed number of levels up
|
|
3196
|
+
* from outputDir is wrong for arbitrary paths: it escapes the output tree and
|
|
3197
|
+
* can land on an unrelated directory.
|
|
3198
|
+
*
|
|
3199
|
+
* @param {string} outputDir - The rpk pages output directory
|
|
3200
|
+
* @returns {string} Absolute or relative path to the partials directory
|
|
3201
|
+
*/
|
|
3202
|
+
function derivePartialsDir (outputDir) {
|
|
3203
|
+
const normalized = String(outputDir).replace(/\\/g, '/')
|
|
3204
|
+
const pagesIndex = normalized.lastIndexOf('/pages/')
|
|
3205
|
+
if (pagesIndex !== -1) {
|
|
3206
|
+
return path.join(normalized.substring(0, pagesIndex), 'partials')
|
|
3207
|
+
}
|
|
3208
|
+
return path.join(path.dirname(normalized), 'partials')
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
/**
|
|
3212
|
+
* Write the -X -> RPK_* env vars partial from a command tree's x_options.
|
|
3213
|
+
* @param {Object} tree - Parsed rpk command tree
|
|
3214
|
+
* @param {string} partialsDir - Directory the partial is written into
|
|
3215
|
+
* @returns {{written: boolean, keyCount?: number, output?: string}}
|
|
3216
|
+
*/
|
|
3217
|
+
function writeEnvVarsPartial (tree, partialsDir) {
|
|
3218
|
+
if (!Array.isArray(tree.x_options) || tree.x_options.length === 0) {
|
|
3219
|
+
return { written: false }
|
|
3220
|
+
}
|
|
3221
|
+
const { renderPartial, keyToEnvVar } = require('./generate-x-env-partial.js')
|
|
3222
|
+
const options = tree.x_options.map(o => ({ name: o.name, env: o.env || keyToEnvVar(o.name) }))
|
|
3223
|
+
const output = path.join(partialsDir, 'rpk-env-vars.adoc')
|
|
3224
|
+
fs.mkdirSync(path.dirname(output), { recursive: true })
|
|
3225
|
+
fs.writeFileSync(output, renderPartial(options))
|
|
3226
|
+
return { written: true, keyCount: options.length, output }
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3169
3229
|
module.exports = {
|
|
3170
3230
|
generateRpkDocs,
|
|
3231
|
+
writeEnvVarsPartial,
|
|
3232
|
+
derivePartialsDir,
|
|
3171
3233
|
mergeCommandOverrides,
|
|
3172
3234
|
applyOverridesToTree,
|
|
3173
3235
|
resolveReferences,
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generates the rpk env vars partial (the `-X` option -> RPK_* environment
|
|
5
|
+
* variable mapping table) from rpk itself, so the table cannot drift from
|
|
6
|
+
* the CLI.
|
|
7
|
+
*
|
|
8
|
+
* Data source, in order of preference:
|
|
9
|
+
* 1. The `x_options` array in `rpk --print-tree` JSON (added in
|
|
10
|
+
* redpanda#31520): structured, versioned with the release, and carries
|
|
11
|
+
* descriptions.
|
|
12
|
+
* 2. `rpk -X list` text output, for rpk versions that predate x_options.
|
|
13
|
+
*
|
|
14
|
+
* The partial is included by both reference:rpk/rpk-x-options.adoc and
|
|
15
|
+
* reference:environment-variables.adoc in the docs repo. Hidden -X options
|
|
16
|
+
* (for example cloud_environment, whose values are deliberately
|
|
17
|
+
* undocumented) appear in neither source, so they are excluded
|
|
18
|
+
* automatically.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs')
|
|
22
|
+
const os = require('os')
|
|
23
|
+
const path = require('path')
|
|
24
|
+
const { spawnSync } = require('child_process')
|
|
25
|
+
|
|
26
|
+
// A healthy rpk exposes ~30 -X options. Refuse to overwrite the partial
|
|
27
|
+
// with a suspiciously small table (for example, if -X list output ever
|
|
28
|
+
// changes format and only partially parses).
|
|
29
|
+
const MIN_OPTIONS = 20
|
|
30
|
+
|
|
31
|
+
const GENERATED_BANNER = `// tag::generated[]
|
|
32
|
+
// Generated from rpk's own -X option data. Do not edit manually:
|
|
33
|
+
// regenerate with doc-tools generate rpk-docs (which rewrites this
|
|
34
|
+
// file alongside the rpk command pages) or, for a targeted refresh,
|
|
35
|
+
// doc-tools generate rpk-env-partial.
|
|
36
|
+
`
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extract -X options from `rpk --print-tree` JSON.
|
|
40
|
+
* @param {string} output - Raw stdout from `rpk --print-tree`
|
|
41
|
+
* @returns {Array<{name: string, env: string}>|null} options in display
|
|
42
|
+
* order, or null when the tree has no x_options (rpk predates
|
|
43
|
+
* redpanda#31520). The env name comes from rpk itself when present
|
|
44
|
+
* (the tree's env field is produced by the same derivation rpk reads).
|
|
45
|
+
*/
|
|
46
|
+
function xOptionsFromTree (output) {
|
|
47
|
+
let tree
|
|
48
|
+
try {
|
|
49
|
+
tree = JSON.parse(output)
|
|
50
|
+
} catch {
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
if (!Array.isArray(tree.x_options) || tree.x_options.length === 0) return null
|
|
54
|
+
return tree.x_options.map(o => ({ name: o.name, env: o.env || keyToEnvVar(o.name) }))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Parse `rpk -X list` output into option keys (fallback for rpk versions
|
|
59
|
+
* whose --print-tree has no x_options).
|
|
60
|
+
* Each line has the form `key=value-hint`; keys contain lowercase
|
|
61
|
+
* letters, digits, dots, and underscores.
|
|
62
|
+
* @param {string} output - Raw stdout from `rpk -X list`
|
|
63
|
+
* @returns {Array<string>} option keys in CLI order
|
|
64
|
+
*/
|
|
65
|
+
function parseXList (output) {
|
|
66
|
+
const keys = []
|
|
67
|
+
for (const line of output.split('\n')) {
|
|
68
|
+
const eq = line.indexOf('=')
|
|
69
|
+
if (eq < 1) continue
|
|
70
|
+
const key = line.slice(0, eq).trim()
|
|
71
|
+
if (/^[a-z0-9._]+$/.test(key)) keys.push(key)
|
|
72
|
+
}
|
|
73
|
+
return keys
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Convert an -X option key to its RPK_* environment variable name:
|
|
78
|
+
* prefix RPK_, uppercase, dots -> underscores.
|
|
79
|
+
* @param {string} key
|
|
80
|
+
* @returns {string}
|
|
81
|
+
*/
|
|
82
|
+
function keyToEnvVar (key) {
|
|
83
|
+
return 'RPK_' + key.toUpperCase().replace(/\./g, '_')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Convert an -X option key to its section anchor on the rpk-x-options page.
|
|
88
|
+
* The docs repo's AsciiDoc ID convention turns dots into hyphens but keeps
|
|
89
|
+
* underscores (tls.insecure_skip_verify -> tls-insecure_skip_verify),
|
|
90
|
+
* verified against the rendered page IDs. The previous hand-written table
|
|
91
|
+
* hyphenated underscores too, which left 12 of its 31 in-page links dead.
|
|
92
|
+
* @param {string} key
|
|
93
|
+
* @returns {string}
|
|
94
|
+
*/
|
|
95
|
+
function keyToAnchor (key) {
|
|
96
|
+
return key.replace(/\./g, '-')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Render the partial's AsciiDoc content.
|
|
101
|
+
* @param {Array<{name: string, env: string}>} options - -X options
|
|
102
|
+
* @returns {string}
|
|
103
|
+
*/
|
|
104
|
+
function renderPartial (options) {
|
|
105
|
+
const rows = options.map(({ name, env }) => {
|
|
106
|
+
const xrefTarget = `xref:reference:rpk/rpk-x-options.adoc#${keyToAnchor(name)}[${name}]`
|
|
107
|
+
return `|${xrefTarget} |${env}`
|
|
108
|
+
})
|
|
109
|
+
return `${GENERATED_BANNER}
|
|
110
|
+
Every \`-X\` option has a corresponding \`RPK_*\` environment variable. Convert by prefixing with \`RPK_\` and replacing dots with underscores:
|
|
111
|
+
|
|
112
|
+
[cols="1m,1m"]
|
|
113
|
+
|===
|
|
114
|
+
|\`-X\` Option |Environment Variable
|
|
115
|
+
|
|
116
|
+
${rows.join('\n')}
|
|
117
|
+
|===
|
|
118
|
+
// end::generated[]
|
|
119
|
+
`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Run an rpk invocation from a Go source checkout.
|
|
124
|
+
* @param {string} sourcePath - Path to src/go/rpk in a redpanda checkout
|
|
125
|
+
* @param {Array<string>} args - rpk arguments
|
|
126
|
+
* @returns {string} Raw stdout
|
|
127
|
+
*/
|
|
128
|
+
function runRpkFromSource (sourcePath, args) {
|
|
129
|
+
const result = spawnSync('go', ['run', 'cmd/rpk/main.go', ...args], {
|
|
130
|
+
cwd: sourcePath,
|
|
131
|
+
encoding: 'utf8',
|
|
132
|
+
timeout: 300000, // includes build time
|
|
133
|
+
maxBuffer: 50 * 1024 * 1024
|
|
134
|
+
})
|
|
135
|
+
if (result.status !== 0) {
|
|
136
|
+
throw new Error(`Failed to run rpk ${args.join(' ')} from source: ${result.stderr}`)
|
|
137
|
+
}
|
|
138
|
+
return result.stdout
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Run an rpk invocation with an existing binary.
|
|
143
|
+
* @param {string} rpkBin - Path to the rpk binary
|
|
144
|
+
* @param {Array<string>} args - rpk arguments
|
|
145
|
+
* @returns {string} Raw stdout
|
|
146
|
+
*/
|
|
147
|
+
function runRpkBinary (rpkBin, args) {
|
|
148
|
+
const result = spawnSync(rpkBin, args, {
|
|
149
|
+
encoding: 'utf8',
|
|
150
|
+
timeout: 60000,
|
|
151
|
+
maxBuffer: 50 * 1024 * 1024
|
|
152
|
+
})
|
|
153
|
+
if (result.status !== 0) {
|
|
154
|
+
throw new Error(`Failed to run ${rpkBin} ${args.join(' ')}: ${result.stderr}`)
|
|
155
|
+
}
|
|
156
|
+
return result.stdout
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Resolve a local rpk source path (repo root or src/go/rpk) without
|
|
161
|
+
* touching the checkout.
|
|
162
|
+
* @param {string} p - User-supplied path
|
|
163
|
+
* @returns {string} Path to src/go/rpk
|
|
164
|
+
*/
|
|
165
|
+
function resolveRpkSourcePath (p) {
|
|
166
|
+
const abs = path.resolve(p)
|
|
167
|
+
if (abs.endsWith(path.join('src', 'go', 'rpk'))) return abs
|
|
168
|
+
const nested = path.join(abs, 'src', 'go', 'rpk')
|
|
169
|
+
if (fs.existsSync(nested)) return nested
|
|
170
|
+
throw new Error(`Cannot find rpk source under ${abs}: pass the repo root or the src/go/rpk directory`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Find the temp sparse-clone root that prepareSourceFromRef created
|
|
175
|
+
* (os.tmpdir()/rpk-source-<random>/redpanda/src/go/rpk), or null when the path is
|
|
176
|
+
* not one of ours — never delete anything outside the temp dir.
|
|
177
|
+
* @param {string} sourcePath - The src/go/rpk path returned by prepareSourceFromRef
|
|
178
|
+
* @returns {string|null}
|
|
179
|
+
*/
|
|
180
|
+
function tempCloneRoot (sourcePath) {
|
|
181
|
+
let dir = sourcePath
|
|
182
|
+
const tmp = fs.realpathSync(os.tmpdir())
|
|
183
|
+
const real = fs.realpathSync(dir)
|
|
184
|
+
dir = real
|
|
185
|
+
while (dir.startsWith(tmp) && dir !== tmp) {
|
|
186
|
+
if (path.basename(dir).startsWith('rpk-source-') && path.dirname(dir) === tmp) return dir
|
|
187
|
+
dir = path.dirname(dir)
|
|
188
|
+
}
|
|
189
|
+
return null
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Generate the partial and write it to disk.
|
|
194
|
+
* @param {Object} options
|
|
195
|
+
* @param {string} [options.ref] - Git ref of redpanda to build rpk from
|
|
196
|
+
* @param {string} [options.fromSource] - Local src/go/rpk path (skips clone)
|
|
197
|
+
* @param {string} [options.rpkBin] - Existing rpk binary (skips build)
|
|
198
|
+
* @param {string} [options.fromJson] - Versioned tree snapshot from docs-data
|
|
199
|
+
* (skips clone and build; the snapshot must carry x_options)
|
|
200
|
+
* @param {string} options.output - Path to write the partial to
|
|
201
|
+
* @returns {{keyCount: number, output: string, source: string}}
|
|
202
|
+
*/
|
|
203
|
+
function handleXEnvPartialGeneration (options) {
|
|
204
|
+
const { ref, fromSource, rpkBin, fromJson, output } = options
|
|
205
|
+
if (!output) throw new Error('Missing required --output path')
|
|
206
|
+
|
|
207
|
+
let xopts
|
|
208
|
+
let source
|
|
209
|
+
if (fromJson) {
|
|
210
|
+
// Snapshots saved by the rpk-docs handler wrap the tree in raw_tree/tree;
|
|
211
|
+
// accept a bare tree too. There is no rpk to run in this mode, so a
|
|
212
|
+
// snapshot from before x_options existed (redpanda#31520) cannot be used.
|
|
213
|
+
const j = JSON.parse(fs.readFileSync(fromJson, 'utf8'))
|
|
214
|
+
for (const candidate of [j.raw_tree, j.tree, j]) {
|
|
215
|
+
if (!candidate) continue
|
|
216
|
+
xopts = xOptionsFromTree(JSON.stringify(candidate))
|
|
217
|
+
if (xopts) break
|
|
218
|
+
}
|
|
219
|
+
if (!xopts) {
|
|
220
|
+
throw new Error(
|
|
221
|
+
`${fromJson} has no x_options; the snapshot predates redpanda#31520. ` +
|
|
222
|
+
'Regenerate with --ref/--from-source/--rpk-bin instead.'
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
source = `snapshot ${path.basename(fromJson)}`
|
|
226
|
+
} else {
|
|
227
|
+
let run
|
|
228
|
+
let cleanupDir = null
|
|
229
|
+
if (rpkBin) {
|
|
230
|
+
run = args => runRpkBinary(rpkBin, args)
|
|
231
|
+
} else if (fromSource && !ref) {
|
|
232
|
+
// Local-source mode with no explicit ref: use the checkout exactly as
|
|
233
|
+
// it stands. prepareSourceFromRef would fetch and check out a ref,
|
|
234
|
+
// silently moving the user's working tree.
|
|
235
|
+
const sourcePath = resolveRpkSourcePath(fromSource)
|
|
236
|
+
run = args => runRpkFromSource(sourcePath, args)
|
|
237
|
+
} else {
|
|
238
|
+
const { prepareSourceFromRef } = require('./rpk-docs-handler.js')
|
|
239
|
+
const sourcePath = prepareSourceFromRef(ref || 'dev', fromSource || null)
|
|
240
|
+
if (!fromSource) {
|
|
241
|
+
// prepareSourceFromRef sparse-cloned into a temp dir this run owns;
|
|
242
|
+
// remove it when done so repeated runs don't accumulate clones.
|
|
243
|
+
cleanupDir = tempCloneRoot(sourcePath)
|
|
244
|
+
}
|
|
245
|
+
run = args => runRpkFromSource(sourcePath, args)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
// Preferred: structured x_options from the command tree JSON.
|
|
250
|
+
xopts = xOptionsFromTree(run(['--print-tree']))
|
|
251
|
+
source = 'print-tree x_options'
|
|
252
|
+
if (!xopts) {
|
|
253
|
+
// Fallback for rpk versions that predate x_options in --print-tree.
|
|
254
|
+
// The env name is derived locally with the documented mapping rule.
|
|
255
|
+
xopts = parseXList(run(['-X', 'list'])).map(name => ({ name, env: keyToEnvVar(name) }))
|
|
256
|
+
source = '-X list (fallback)'
|
|
257
|
+
}
|
|
258
|
+
} finally {
|
|
259
|
+
if (cleanupDir) {
|
|
260
|
+
fs.rmSync(cleanupDir, { recursive: true, force: true })
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (xopts.length < MIN_OPTIONS) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
`Parsed only ${xopts.length} -X options from ${source} (expected at least ${MIN_OPTIONS}); ` +
|
|
268
|
+
'refusing to write a suspiciously small partial. The output format may have changed.'
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
fs.mkdirSync(path.dirname(output), { recursive: true })
|
|
273
|
+
fs.writeFileSync(output, renderPartial(xopts))
|
|
274
|
+
return { keyCount: xopts.length, output, source }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
module.exports = {
|
|
278
|
+
xOptionsFromTree,
|
|
279
|
+
parseXList,
|
|
280
|
+
keyToEnvVar,
|
|
281
|
+
keyToAnchor,
|
|
282
|
+
renderPartial,
|
|
283
|
+
handleXEnvPartialGeneration
|
|
284
|
+
}
|
|
@@ -7,7 +7,7 @@ const fs = require('fs')
|
|
|
7
7
|
const os = require('os')
|
|
8
8
|
const semver = require('semver')
|
|
9
9
|
const { findRepoRoot } = require('../../cli-utils/doc-tools-utils')
|
|
10
|
-
const { generateRpkDocs, applyOverridesToTree, resolveReferences, shouldExcludeCommand, shouldUsePartialDir } = require('./generate-rpk-docs')
|
|
10
|
+
const { generateRpkDocs, applyOverridesToTree, resolveReferences, shouldExcludeCommand, shouldUsePartialDir, derivePartialsDir } = require('./generate-rpk-docs')
|
|
11
11
|
const { detectLinuxOnlyFromSource, warnIfDetectionLooksBroken } = require('./detect-platform-commands')
|
|
12
12
|
const { generateRpkDiff, printDiffReport, generateWhatsNewSection, flattenToMap } = require('./report-delta')
|
|
13
13
|
const { loadAndValidateOverrides, ValidationResult } = require('./validate-overrides')
|
|
@@ -1914,26 +1914,14 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1914
1914
|
const defaultOutputDir = path.join(repoRoot, 'modules', 'reference', 'pages', 'rpk')
|
|
1915
1915
|
const finalOutputDir = outputDir || defaultOutputDir
|
|
1916
1916
|
|
|
1917
|
-
//
|
|
1918
|
-
//
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
const pagesIndex = normalizedOutput.lastIndexOf('/pages/')
|
|
1926
|
-
const referenceDir = normalizedOutput.substring(0, pagesIndex)
|
|
1927
|
-
defaultCloudSecretDir = path.join(referenceDir, 'partials')
|
|
1928
|
-
} else {
|
|
1929
|
-
// Arbitrary output directory - use sibling partials directory
|
|
1930
|
-
const outputParent = path.dirname(finalOutputDir)
|
|
1931
|
-
defaultCloudSecretDir = path.join(outputParent, 'partials')
|
|
1932
|
-
}
|
|
1933
|
-
} else {
|
|
1934
|
-
defaultCloudSecretDir = path.join(repoRoot, 'modules', 'reference', 'partials')
|
|
1935
|
-
}
|
|
1936
|
-
const finalCloudSecretDir = cloudSecretDir || defaultCloudSecretDir
|
|
1917
|
+
// Partials live relative to outputDir, not repoRoot: for the docs layout
|
|
1918
|
+
// (.../modules/reference/pages/rpk) that is .../modules/reference/partials,
|
|
1919
|
+
// and for an arbitrary --output-dir it is a partials dir beside the output.
|
|
1920
|
+
// derivePartialsDir owns that rule so each consumer does not re-derive it.
|
|
1921
|
+
const derivedPartialsDir = outputDir
|
|
1922
|
+
? derivePartialsDir(finalOutputDir)
|
|
1923
|
+
: path.join(repoRoot, 'modules', 'reference', 'partials')
|
|
1924
|
+
const finalCloudSecretDir = cloudSecretDir || derivedPartialsDir
|
|
1937
1925
|
|
|
1938
1926
|
// nav.adoc lives at modules/ROOT/nav.adoc relative to the repo root
|
|
1939
1927
|
const navFile = path.join(repoRoot, 'modules', 'ROOT', 'nav.adoc')
|
|
@@ -2139,6 +2127,7 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
2139
2127
|
overrides: overridesData,
|
|
2140
2128
|
outputDir: finalOutputDir,
|
|
2141
2129
|
cloudSecretDir: finalCloudSecretDir,
|
|
2130
|
+
envPartialDir: derivedPartialsDir,
|
|
2142
2131
|
rpkVersion,
|
|
2143
2132
|
pluginVersions,
|
|
2144
2133
|
draftMissing,
|
|
@@ -2526,6 +2515,7 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
2526
2515
|
overrides: overridesData,
|
|
2527
2516
|
outputDir: finalOutputDir,
|
|
2528
2517
|
cloudSecretDir: finalCloudSecretDir,
|
|
2518
|
+
envPartialDir: derivedPartialsDir,
|
|
2529
2519
|
rpkVersion,
|
|
2530
2520
|
pluginVersions,
|
|
2531
2521
|
draftMissing,
|