@redpanda-data/docs-extensions-and-macros 5.10.0 → 5.11.1

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.
@@ -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
- // cloudSecretDir should be relative to outputDir, not repoRoot
1918
- // If outputDir is .../modules/reference/pages/rpk, cloudSecretDir should be .../modules/reference/partials
1919
- let defaultCloudSecretDir
1920
- if (outputDir) {
1921
- // Check if outputDir follows docs repo structure (ends with pages/rpk or similar)
1922
- const normalizedOutput = finalOutputDir.replace(/\\/g, '/')
1923
- if (normalizedOutput.includes('/pages/')) {
1924
- // Derive cloudSecretDir from outputDir by going up from pages/rpk to partials
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,