@redpanda-data/docs-extensions-and-macros 5.3.0 → 5.3.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.js +46 -8
- package/cli-utils/diff-utils.js +42 -7
- package/package.json +1 -1
- package/tools/property-extractor/Makefile +46 -12
- package/tools/property-extractor/cloud_config.py +43 -14
- package/tools/property-extractor/compare-properties.js +4 -3
- package/tools/property-extractor/pr-summary-formatter.js +7 -4
- package/tools/property-extractor/property_extractor.py +102 -7
- package/tools/rpk-docs/detect-platform-commands.js +566 -0
- package/tools/rpk-docs/generate-rpk-docs.js +33 -12
- package/tools/rpk-docs/rpk-docs-handler.js +105 -112
- package/tools/rpk-docs/templates/command.hbs +12 -0
- package/tools/rpk-docs/validate-overrides.js +16 -0
- package/tools/property-extractor/templates/property-backup.hbs +0 -161
- package/tools/property-extractor/templates/topic-property-backup.hbs +0 -148
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Static (source-based) detection of Linux-only rpk commands.
|
|
5
|
+
*
|
|
6
|
+
* Works on any platform (including Linux CI runners, where the dynamic
|
|
7
|
+
* "build on Linux and Darwin and diff the trees" approach is impossible
|
|
8
|
+
* because a cross-compiled darwin rpk cannot be executed).
|
|
9
|
+
*
|
|
10
|
+
* How it works
|
|
11
|
+
* ------------
|
|
12
|
+
* rpk gates platform-specific commands with Go build constraints:
|
|
13
|
+
*
|
|
14
|
+
* 1. Whole-package gating: every file in a command's package carries a
|
|
15
|
+
* Linux constraint (explicit `//go:build linux` tag or a `_linux.go`
|
|
16
|
+
* filename suffix) and no darwin-buildable counterpart exists.
|
|
17
|
+
* Example: pkg/cli/iotune, pkg/cli/redpanda/tune.
|
|
18
|
+
*
|
|
19
|
+
* 2. Dual registration: a package provides two variants of the same
|
|
20
|
+
* command constructor, one per platform, registering different
|
|
21
|
+
* subcommand sets. Example: pkg/cli/redpanda/redpanda.go
|
|
22
|
+
* (`//go:build linux`, registers start/stop/check/mode/config/tune)
|
|
23
|
+
* vs pkg/cli/redpanda/redpanda_darwin.go (`//go:build darwin`,
|
|
24
|
+
* registers only the hidden admin command). Same pattern at the root:
|
|
25
|
+
* pkg/cli/root_linux.go vs pkg/cli/root_darwin.go.
|
|
26
|
+
*
|
|
27
|
+
* This module detects both patterns:
|
|
28
|
+
*
|
|
29
|
+
* - For every scanned package, it computes which files build on linux
|
|
30
|
+
* and which build on darwin (from `//go:build` / `// +build` tags AND
|
|
31
|
+
* filename-implied constraints like `_linux.go`).
|
|
32
|
+
* - Packages whose files all fail to build on darwin (but build on
|
|
33
|
+
* linux) and that define a cobra command constructor are Linux-only.
|
|
34
|
+
* - Packages containing platform-differential files get a registration
|
|
35
|
+
* diff: subcommand constructors referenced from linux-buildable files
|
|
36
|
+
* but not from darwin-buildable files are Linux-only. Deprecated and
|
|
37
|
+
* hidden constructors are ignored because `rpk --print-tree` (the
|
|
38
|
+
* basis of the generated docs) excludes hidden commands.
|
|
39
|
+
*
|
|
40
|
+
* Scope: built-in commands only. Plugin commands (rpk connect, rpk ai, ...)
|
|
41
|
+
* are not part of the rpk source tree, so their platform availability
|
|
42
|
+
* cannot be derived here. Plugins keep whatever platform behavior the
|
|
43
|
+
* caller's other detection paths provide (for example, dynamic detection
|
|
44
|
+
* marks plugin-only commands when plugins are only installed in the Linux
|
|
45
|
+
* build container).
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
const fs = require('fs')
|
|
49
|
+
const path = require('path')
|
|
50
|
+
|
|
51
|
+
/** GOOS values recognized in filename suffixes and build tags */
|
|
52
|
+
const KNOWN_GOOS = new Set([
|
|
53
|
+
'aix', 'android', 'darwin', 'dragonfly', 'freebsd', 'hurd', 'illumos',
|
|
54
|
+
'ios', 'js', 'linux', 'nacl', 'netbsd', 'openbsd', 'plan9', 'solaris',
|
|
55
|
+
'wasip1', 'windows', 'zos'
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
/** GOOS values that satisfy the `unix` build tag */
|
|
59
|
+
const UNIX_GOOS = new Set([
|
|
60
|
+
'aix', 'android', 'darwin', 'dragonfly', 'freebsd', 'hurd', 'illumos',
|
|
61
|
+
'ios', 'linux', 'netbsd', 'openbsd', 'solaris'
|
|
62
|
+
])
|
|
63
|
+
|
|
64
|
+
/** GOARCH values recognized in filename suffixes */
|
|
65
|
+
const KNOWN_GOARCH = new Set([
|
|
66
|
+
'386', 'amd64', 'amd64p32', 'arm', 'arm64', 'arm64be', 'armbe', 'loong64',
|
|
67
|
+
'mips', 'mips64', 'mips64le', 'mips64p32', 'mips64p32le', 'mipsle',
|
|
68
|
+
'ppc', 'ppc64', 'ppc64le', 'riscv', 'riscv64', 's390', 's390x',
|
|
69
|
+
'sparc', 'sparc64', 'wasm'
|
|
70
|
+
])
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Directories (relative to the rpk source root) scanned for command
|
|
74
|
+
* implementations. Modern rpk keeps commands in pkg/cli/<command>/;
|
|
75
|
+
* the other roots cover older source layouts.
|
|
76
|
+
*/
|
|
77
|
+
const SCAN_ROOTS = ['pkg/cli', 'pkg/cli/cmd', 'cmd/rpk']
|
|
78
|
+
|
|
79
|
+
/** Directory names that never correspond to a command path segment */
|
|
80
|
+
const NON_COMMAND_SEGMENTS = new Set(['internal', 'common', 'testdata'])
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Determine the GOOS constraint implied by a Go filename.
|
|
84
|
+
* Follows Go's rules: *_GOOS.go, *_GOOS_GOARCH.go constrain the OS;
|
|
85
|
+
* *_GOARCH.go alone does not.
|
|
86
|
+
* @param {string} fileName - e.g. 'redpanda_darwin.go', 'bundle_k8s_linux.go'
|
|
87
|
+
* @returns {string|null} GOOS name or null when unconstrained
|
|
88
|
+
*/
|
|
89
|
+
function goosFromFileName(fileName) {
|
|
90
|
+
const base = fileName.replace(/\.go$/, '')
|
|
91
|
+
const parts = base.split('_')
|
|
92
|
+
if (parts.length < 2) return null
|
|
93
|
+
|
|
94
|
+
const last = parts[parts.length - 1]
|
|
95
|
+
if (KNOWN_GOOS.has(last)) return last
|
|
96
|
+
if (KNOWN_GOARCH.has(last) && parts.length >= 3) {
|
|
97
|
+
const secondLast = parts[parts.length - 2]
|
|
98
|
+
if (KNOWN_GOOS.has(secondLast)) return secondLast
|
|
99
|
+
}
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Evaluate a `//go:build` constraint expression for a target GOOS.
|
|
105
|
+
* Supports identifiers, !, &&, || and parentheses.
|
|
106
|
+
* Unknown identifiers (custom build tags) evaluate to false, matching a
|
|
107
|
+
* default `go build` with no -tags flag. GOARCH identifiers evaluate to
|
|
108
|
+
* true because we only care about OS-level availability.
|
|
109
|
+
* @param {string} expr - Expression after `//go:build`
|
|
110
|
+
* @param {string} goos - Target GOOS ('linux' or 'darwin')
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
function evaluateBuildExpr(expr, goos) {
|
|
114
|
+
const tokens = expr.match(/[A-Za-z0-9_.]+|&&|\|\||!|\(|\)/g) || []
|
|
115
|
+
let pos = 0
|
|
116
|
+
|
|
117
|
+
const peek = () => tokens[pos]
|
|
118
|
+
const next = () => tokens[pos++]
|
|
119
|
+
|
|
120
|
+
function evalIdent(ident) {
|
|
121
|
+
if (KNOWN_GOOS.has(ident)) return ident === goos
|
|
122
|
+
if (ident === 'unix') return UNIX_GOOS.has(goos)
|
|
123
|
+
if (KNOWN_GOARCH.has(ident)) return true
|
|
124
|
+
if (ident === 'cgo') return true
|
|
125
|
+
// Custom build tags (e.g. withasan, integration) are unset by default
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parsePrimary() {
|
|
130
|
+
const tok = next()
|
|
131
|
+
if (tok === '(') {
|
|
132
|
+
const val = parseOr()
|
|
133
|
+
if (peek() === ')') next()
|
|
134
|
+
return val
|
|
135
|
+
}
|
|
136
|
+
if (tok === '!') return !parsePrimary()
|
|
137
|
+
if (tok === undefined) return false
|
|
138
|
+
return evalIdent(tok)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function parseAnd() {
|
|
142
|
+
let val = parsePrimary()
|
|
143
|
+
while (peek() === '&&') {
|
|
144
|
+
next()
|
|
145
|
+
val = parsePrimary() && val
|
|
146
|
+
}
|
|
147
|
+
return val
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function parseOr() {
|
|
151
|
+
let val = parseAnd()
|
|
152
|
+
while (peek() === '||') {
|
|
153
|
+
next()
|
|
154
|
+
val = parseAnd() || val
|
|
155
|
+
}
|
|
156
|
+
return val
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return parseOr()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Evaluate legacy `// +build` lines for a target GOOS.
|
|
164
|
+
* Multiple lines AND together; within a line, space-separated options OR
|
|
165
|
+
* and comma-separated terms AND.
|
|
166
|
+
* @param {string[]} lines - Contents after `// +build` (one per line)
|
|
167
|
+
* @param {string} goos - Target GOOS
|
|
168
|
+
* @returns {boolean}
|
|
169
|
+
*/
|
|
170
|
+
function evaluatePlusBuildLines(lines, goos) {
|
|
171
|
+
const evalTerm = (term) => {
|
|
172
|
+
let negate = false
|
|
173
|
+
while (term.startsWith('!')) {
|
|
174
|
+
negate = !negate
|
|
175
|
+
term = term.slice(1)
|
|
176
|
+
}
|
|
177
|
+
let val
|
|
178
|
+
if (KNOWN_GOOS.has(term)) val = term === goos
|
|
179
|
+
else if (term === 'unix') val = UNIX_GOOS.has(goos)
|
|
180
|
+
else if (KNOWN_GOARCH.has(term) || term === 'cgo') val = true
|
|
181
|
+
else val = false
|
|
182
|
+
return negate ? !val : val
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return lines.every(line => {
|
|
186
|
+
const options = line.trim().split(/\s+/).filter(Boolean)
|
|
187
|
+
if (options.length === 0) return true
|
|
188
|
+
return options.some(opt => opt.split(',').every(evalTerm))
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Extract the build constraint (if any) from a Go file's header.
|
|
194
|
+
* Only lines before the `package` declaration are considered.
|
|
195
|
+
* @param {string} content - Go file content
|
|
196
|
+
* @returns {{goBuildExpr: string|null, plusBuildLines: string[]}}
|
|
197
|
+
*/
|
|
198
|
+
function extractBuildConstraint(content) {
|
|
199
|
+
const header = content.split(/^package\s/m)[0]
|
|
200
|
+
const goBuildMatch = header.match(/^\/\/go:build\s+(.+)$/m)
|
|
201
|
+
const plusBuildLines = []
|
|
202
|
+
const plusBuildRe = /^\/\/\s*\+build\s+(.+)$/gm
|
|
203
|
+
let m
|
|
204
|
+
while ((m = plusBuildRe.exec(header)) !== null) {
|
|
205
|
+
plusBuildLines.push(m[1])
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
goBuildExpr: goBuildMatch ? goBuildMatch[1].trim() : null,
|
|
209
|
+
plusBuildLines
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Determine whether a Go file builds for a target GOOS, taking both the
|
|
215
|
+
* filename-implied constraint and explicit build tags into account.
|
|
216
|
+
* @param {string} fileName - Base name of the file
|
|
217
|
+
* @param {string} content - File content
|
|
218
|
+
* @param {string} goos - Target GOOS ('linux' or 'darwin')
|
|
219
|
+
* @returns {boolean}
|
|
220
|
+
*/
|
|
221
|
+
function fileBuildsOn(fileName, content, goos) {
|
|
222
|
+
const fileGoos = goosFromFileName(fileName)
|
|
223
|
+
if (fileGoos && fileGoos !== goos) return false
|
|
224
|
+
|
|
225
|
+
const { goBuildExpr, plusBuildLines } = extractBuildConstraint(content)
|
|
226
|
+
if (goBuildExpr) return evaluateBuildExpr(goBuildExpr, goos)
|
|
227
|
+
if (plusBuildLines.length > 0) return evaluatePlusBuildLines(plusBuildLines, goos)
|
|
228
|
+
return true
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Derive a command name from a cobra constructor function name.
|
|
233
|
+
* NewStartCommand -> start, newTuneCommand -> tune, NewCommand -> null.
|
|
234
|
+
* @param {string} funcName
|
|
235
|
+
* @returns {string|null}
|
|
236
|
+
*/
|
|
237
|
+
function commandNameFromConstructor(funcName) {
|
|
238
|
+
const m = funcName.match(/^(?:New|new)(\w+?)(?:Command|Cmd)$/)
|
|
239
|
+
if (m && m[1]) return m[1].toLowerCase()
|
|
240
|
+
return null
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Parse cobra command constructors defined in a Go file.
|
|
245
|
+
* A constructor is any `func XxxYyy(...) *cobra.Command`. For each one we
|
|
246
|
+
* record the cobra `Use:` name (first token) and whether the command is
|
|
247
|
+
* deprecated or hidden (such commands never appear in `rpk --print-tree`).
|
|
248
|
+
* @param {string} content - Go file content
|
|
249
|
+
* @returns {Object<string, {useName: string|null, excluded: boolean}>}
|
|
250
|
+
*/
|
|
251
|
+
function parseConstructors(content) {
|
|
252
|
+
const constructors = {}
|
|
253
|
+
const funcRe = /^func\s+(\w+)\s*\([^)]*\)\s*\*cobra\.Command\s*\{/gm
|
|
254
|
+
const matches = []
|
|
255
|
+
let m
|
|
256
|
+
while ((m = funcRe.exec(content)) !== null) {
|
|
257
|
+
matches.push({ name: m[1], start: m.index })
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
for (let i = 0; i < matches.length; i++) {
|
|
261
|
+
const bodyEnd = i + 1 < matches.length ? matches[i + 1].start : content.length
|
|
262
|
+
const body = content.slice(matches[i].start, bodyEnd)
|
|
263
|
+
|
|
264
|
+
const useMatch = body.match(/\bUse:\s*"([^"]+)"/)
|
|
265
|
+
const useName = useMatch ? useMatch[1].trim().split(/\s+/)[0] : null
|
|
266
|
+
|
|
267
|
+
// Deprecated or hidden commands are excluded from `rpk --print-tree`
|
|
268
|
+
const excluded = /\bHidden:\s*true\b/.test(body) ||
|
|
269
|
+
/\bDeprecated:\s*"/.test(body) ||
|
|
270
|
+
/\bDeprecateCmd\s*\(/.test(body)
|
|
271
|
+
|
|
272
|
+
constructors[matches[i].name] = { useName, excluded }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return constructors
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Extract constructor references from all AddCommand(...) calls in a file.
|
|
280
|
+
* Handles both same-package (`NewStartCommand(...)`) and cross-package
|
|
281
|
+
* (`tune.NewCommand(...)`) references. Only functions starting with
|
|
282
|
+
* New/new are considered, which skips wrappers like cobraext.DeprecateCmd.
|
|
283
|
+
* @param {string} content - Go file content
|
|
284
|
+
* @returns {Array<{qualifier: string|null, funcName: string}>}
|
|
285
|
+
*/
|
|
286
|
+
function parseAddCommandRefs(content) {
|
|
287
|
+
const refs = []
|
|
288
|
+
const addRe = /\bAddCommand\s*\(/g
|
|
289
|
+
let m
|
|
290
|
+
while ((m = addRe.exec(content)) !== null) {
|
|
291
|
+
// Find the span of this AddCommand(...) call by balancing parentheses
|
|
292
|
+
let depth = 1
|
|
293
|
+
let i = m.index + m[0].length
|
|
294
|
+
while (i < content.length && depth > 0) {
|
|
295
|
+
if (content[i] === '(') depth++
|
|
296
|
+
else if (content[i] === ')') depth--
|
|
297
|
+
i++
|
|
298
|
+
}
|
|
299
|
+
const span = content.slice(m.index + m[0].length, i - 1)
|
|
300
|
+
|
|
301
|
+
const callRe = /(?:(\w+)\.)?((?:New|new)\w*)\s*\(/g
|
|
302
|
+
let c
|
|
303
|
+
while ((c = callRe.exec(span)) !== null) {
|
|
304
|
+
refs.push({ qualifier: c[1] || null, funcName: c[2] })
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return refs
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Recursively collect non-test Go files under a directory.
|
|
312
|
+
* @param {string} dir - Absolute directory path
|
|
313
|
+
* @param {Array} out - Accumulator of {absPath, dir, name}
|
|
314
|
+
*/
|
|
315
|
+
function collectGoFiles(dir, out) {
|
|
316
|
+
let entries
|
|
317
|
+
try {
|
|
318
|
+
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
319
|
+
} catch (err) {
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
for (const entry of entries) {
|
|
323
|
+
const fullPath = path.join(dir, entry.name)
|
|
324
|
+
if (entry.isDirectory()) {
|
|
325
|
+
if (entry.name === 'testdata' || entry.name === 'vendor') continue
|
|
326
|
+
collectGoFiles(fullPath, out)
|
|
327
|
+
} else if (entry.name.endsWith('.go') && !entry.name.endsWith('_test.go')) {
|
|
328
|
+
out.push({ absPath: fullPath, dir, name: entry.name })
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Build the command path prefix for a package directory.
|
|
335
|
+
* pkg/cli -> 'rpk'; pkg/cli/redpanda -> 'rpk redpanda';
|
|
336
|
+
* pkg/cli/cmd/redpanda (legacy) -> 'rpk redpanda'.
|
|
337
|
+
* @param {string} scanRoot - Absolute path of the scan root
|
|
338
|
+
* @param {string} dir - Absolute directory path
|
|
339
|
+
* @returns {string|null} Command path prefix, or null for non-command dirs
|
|
340
|
+
*/
|
|
341
|
+
function commandPrefixForDir(scanRoot, dir) {
|
|
342
|
+
const rel = path.relative(scanRoot, dir)
|
|
343
|
+
let segments = rel === '' ? [] : rel.split(path.sep)
|
|
344
|
+
// Legacy layouts nested commands under an extra cmd/ level
|
|
345
|
+
if (segments[0] === 'cmd') segments = segments.slice(1)
|
|
346
|
+
if (segments.some(s => NON_COMMAND_SEGMENTS.has(s))) return null
|
|
347
|
+
return ['rpk', ...segments].join(' ')
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Analyze one scan root and add detected Linux-only command paths.
|
|
352
|
+
* @param {string} scanRoot - Absolute path of the scan root
|
|
353
|
+
* @param {Set<string>} linuxOnlyCommands - Accumulator
|
|
354
|
+
*/
|
|
355
|
+
function scanRootForLinuxOnly(scanRoot, linuxOnlyCommands) {
|
|
356
|
+
const files = []
|
|
357
|
+
collectGoFiles(scanRoot, files)
|
|
358
|
+
if (files.length === 0) return
|
|
359
|
+
|
|
360
|
+
// Group files (with parsed metadata) by directory
|
|
361
|
+
const dirs = new Map()
|
|
362
|
+
for (const file of files) {
|
|
363
|
+
let content
|
|
364
|
+
try {
|
|
365
|
+
content = fs.readFileSync(file.absPath, 'utf8')
|
|
366
|
+
} catch (err) {
|
|
367
|
+
continue
|
|
368
|
+
}
|
|
369
|
+
const info = {
|
|
370
|
+
name: file.name,
|
|
371
|
+
content,
|
|
372
|
+
buildsOnLinux: fileBuildsOn(file.name, content, 'linux'),
|
|
373
|
+
buildsOnDarwin: fileBuildsOn(file.name, content, 'darwin')
|
|
374
|
+
}
|
|
375
|
+
if (!dirs.has(file.dir)) dirs.set(file.dir, [])
|
|
376
|
+
dirs.get(file.dir).push(info)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Per-directory constructor index: dir -> {funcName: {useName, excluded}}
|
|
380
|
+
const constructorIndex = new Map()
|
|
381
|
+
for (const [dir, dirFiles] of dirs) {
|
|
382
|
+
const index = {}
|
|
383
|
+
for (const f of dirFiles) {
|
|
384
|
+
Object.assign(index, parseConstructors(f.content))
|
|
385
|
+
}
|
|
386
|
+
constructorIndex.set(dir, index)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Resolve an AddCommand constructor reference to a visible command name.
|
|
391
|
+
* Returns null for deprecated/hidden/unresolvable commands.
|
|
392
|
+
*/
|
|
393
|
+
const resolveRef = (dir, ref) => {
|
|
394
|
+
let target = null
|
|
395
|
+
let fallback = commandNameFromConstructor(ref.funcName)
|
|
396
|
+
|
|
397
|
+
if (ref.qualifier) {
|
|
398
|
+
const subDir = path.join(dir, ref.qualifier)
|
|
399
|
+
target = constructorIndex.get(subDir) || null
|
|
400
|
+
if (!fallback) fallback = ref.qualifier
|
|
401
|
+
} else {
|
|
402
|
+
target = constructorIndex.get(dir) || null
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (target && target[ref.funcName]) {
|
|
406
|
+
const ctor = target[ref.funcName]
|
|
407
|
+
if (ctor.excluded) return null
|
|
408
|
+
return ctor.useName || fallback
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Unresolvable reference: only trust it if the function name follows
|
|
412
|
+
// the cobra constructor convention (New[Xxx]Command). This skips
|
|
413
|
+
// helper arguments captured inside AddCommand spans, such as the
|
|
414
|
+
// rp.NewLauncher() argument in redpanda.NewCommand(fs, p, rp.NewLauncher()).
|
|
415
|
+
if (!/^(?:New|new)(?:\w*(?:Command|Cmd))?$/.test(ref.funcName)) return null
|
|
416
|
+
return fallback
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
for (const [dir, dirFiles] of dirs) {
|
|
420
|
+
const prefix = commandPrefixForDir(scanRoot, dir)
|
|
421
|
+
if (!prefix) continue
|
|
422
|
+
|
|
423
|
+
const hasDifferentialFile = dirFiles.some(f => f.buildsOnLinux !== f.buildsOnDarwin)
|
|
424
|
+
|
|
425
|
+
// Pattern 1: whole package is Linux-gated (and defines a command)
|
|
426
|
+
if (prefix !== 'rpk') {
|
|
427
|
+
const anyLinux = dirFiles.some(f => f.buildsOnLinux)
|
|
428
|
+
const anyDarwin = dirFiles.some(f => f.buildsOnDarwin)
|
|
429
|
+
const definesCommand = Object.keys(constructorIndex.get(dir)).length > 0
|
|
430
|
+
if (anyLinux && !anyDarwin && definesCommand) {
|
|
431
|
+
linuxOnlyCommands.add(prefix)
|
|
432
|
+
continue
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Pattern 2: dual registration - diff subcommands registered from
|
|
437
|
+
// linux-buildable files vs darwin-buildable files
|
|
438
|
+
if (!hasDifferentialFile) continue
|
|
439
|
+
|
|
440
|
+
const refsFor = (goos) => {
|
|
441
|
+
const names = new Set()
|
|
442
|
+
for (const f of dirFiles) {
|
|
443
|
+
const buildable = goos === 'linux' ? f.buildsOnLinux : f.buildsOnDarwin
|
|
444
|
+
if (!buildable) continue
|
|
445
|
+
for (const ref of parseAddCommandRefs(f.content)) {
|
|
446
|
+
const name = resolveRef(dir, ref)
|
|
447
|
+
if (name) names.add(name)
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return names
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const linuxRefs = refsFor('linux')
|
|
454
|
+
const darwinRefs = refsFor('darwin')
|
|
455
|
+
|
|
456
|
+
for (const name of linuxRefs) {
|
|
457
|
+
if (!darwinRefs.has(name)) {
|
|
458
|
+
linuxOnlyCommands.add(`${prefix} ${name}`)
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Detect Linux-only rpk commands by statically analyzing Go source.
|
|
466
|
+
* Works on any platform, including Linux CI runners.
|
|
467
|
+
* @param {string} sourcePath - Path to the rpk Go source root (src/go/rpk)
|
|
468
|
+
* @returns {Set<string>} Linux-only command paths (e.g. 'rpk iotune').
|
|
469
|
+
* Descendants of a returned path are implicitly Linux-only too.
|
|
470
|
+
*/
|
|
471
|
+
function detectLinuxOnlyFromSource(sourcePath) {
|
|
472
|
+
const linuxOnlyCommands = new Set()
|
|
473
|
+
|
|
474
|
+
const scannedRoots = new Set()
|
|
475
|
+
for (const root of SCAN_ROOTS) {
|
|
476
|
+
const absRoot = path.join(sourcePath, root)
|
|
477
|
+
if (!fs.existsSync(absRoot)) continue
|
|
478
|
+
// pkg/cli/cmd is nested inside pkg/cli; avoid scanning it twice
|
|
479
|
+
if ([...scannedRoots].some(r => absRoot.startsWith(r + path.sep))) continue
|
|
480
|
+
scannedRoots.add(absRoot)
|
|
481
|
+
scanRootForLinuxOnly(absRoot, linuxOnlyCommands)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Drop any detected path that is a descendant of another detected path;
|
|
485
|
+
// platform markers already propagate to descendants.
|
|
486
|
+
for (const cmd of [...linuxOnlyCommands]) {
|
|
487
|
+
if ([...linuxOnlyCommands].some(other => other !== cmd && cmd.startsWith(other + ' '))) {
|
|
488
|
+
linuxOnlyCommands.delete(cmd)
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return linuxOnlyCommands
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* List Go files under the scanned roots that carry a Linux-only build
|
|
497
|
+
* constraint (explicit tag or filename suffix). Used as a tripwire: if
|
|
498
|
+
* detection returns an empty set while this list is non-empty, the scan
|
|
499
|
+
* is almost certainly broken (wrong directory layout, changed tag style).
|
|
500
|
+
* @param {string} sourcePath - Path to the rpk Go source root
|
|
501
|
+
* @returns {string[]} Relative paths of Linux-constrained files
|
|
502
|
+
*/
|
|
503
|
+
function findLinuxConstrainedFiles(sourcePath) {
|
|
504
|
+
const results = []
|
|
505
|
+
for (const root of SCAN_ROOTS) {
|
|
506
|
+
const absRoot = path.join(sourcePath, root)
|
|
507
|
+
if (!fs.existsSync(absRoot)) continue
|
|
508
|
+
const files = []
|
|
509
|
+
collectGoFiles(absRoot, files)
|
|
510
|
+
for (const file of files) {
|
|
511
|
+
let content
|
|
512
|
+
try {
|
|
513
|
+
content = fs.readFileSync(file.absPath, 'utf8')
|
|
514
|
+
} catch (err) {
|
|
515
|
+
continue
|
|
516
|
+
}
|
|
517
|
+
if (fileBuildsOn(file.name, content, 'linux') && !fileBuildsOn(file.name, content, 'darwin')) {
|
|
518
|
+
results.push(path.relative(sourcePath, file.absPath))
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return [...new Set(results)]
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Emit a loud warning when platform detection came back empty even though
|
|
527
|
+
* the source demonstrably contains Linux-gated files. Never let a scan
|
|
528
|
+
* failure silently mark every command as cross-platform.
|
|
529
|
+
* @param {string} sourcePath - Path to the rpk Go source root
|
|
530
|
+
* @param {Set<string>} linuxOnlyCommands - Combined detection result
|
|
531
|
+
* @returns {boolean} True when the tripwire fired
|
|
532
|
+
*/
|
|
533
|
+
function warnIfDetectionLooksBroken(sourcePath, linuxOnlyCommands) {
|
|
534
|
+
if (linuxOnlyCommands.size > 0) return false
|
|
535
|
+
|
|
536
|
+
const constrained = findLinuxConstrainedFiles(sourcePath)
|
|
537
|
+
if (constrained.length === 0) return false
|
|
538
|
+
|
|
539
|
+
console.warn('\n' + '!'.repeat(70))
|
|
540
|
+
console.warn('⚠ PLATFORM DETECTION TRIPWIRE: no Linux-only commands were detected,')
|
|
541
|
+
console.warn(`⚠ but the rpk source contains ${constrained.length} Linux-constrained file(s), e.g.:`)
|
|
542
|
+
for (const file of constrained.slice(0, 5)) {
|
|
543
|
+
console.warn(`⚠ - ${file}`)
|
|
544
|
+
}
|
|
545
|
+
console.warn('⚠ This usually means the static source scan failed (unexpected')
|
|
546
|
+
console.warn('⚠ directory layout or build-tag style) and every command is about')
|
|
547
|
+
console.warn('⚠ to be marked as available on both Linux and macOS, which is wrong.')
|
|
548
|
+
console.warn('⚠ Fix tools/rpk-docs/detect-platform-commands.js before publishing')
|
|
549
|
+
console.warn('⚠ these docs. See redpanda-data/docs#1831 for the impact of this.')
|
|
550
|
+
console.warn('!'.repeat(70) + '\n')
|
|
551
|
+
return true
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
module.exports = {
|
|
555
|
+
detectLinuxOnlyFromSource,
|
|
556
|
+
findLinuxConstrainedFiles,
|
|
557
|
+
warnIfDetectionLooksBroken,
|
|
558
|
+
// Exported for unit tests
|
|
559
|
+
fileBuildsOn,
|
|
560
|
+
goosFromFileName,
|
|
561
|
+
evaluateBuildExpr,
|
|
562
|
+
evaluatePlusBuildLines,
|
|
563
|
+
parseConstructors,
|
|
564
|
+
parseAddCommandRefs,
|
|
565
|
+
commandNameFromConstructor
|
|
566
|
+
}
|
|
@@ -291,6 +291,24 @@ function shouldUsePartialDir(overrides, commandPath) {
|
|
|
291
291
|
return false
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Check if a command belongs to the rpk cloud or rpk security secret families.
|
|
296
|
+
* Their pages are routed to the cloud partials directory (cloudSecretDir) and
|
|
297
|
+
* published by cloud-docs, so links to them from regular pages must cross to
|
|
298
|
+
* the cloud component.
|
|
299
|
+
* @param {string} commandPath - Full command path (e.g., "rpk cloud login")
|
|
300
|
+
* @returns {boolean}
|
|
301
|
+
*/
|
|
302
|
+
function isCloudSecretCommand(commandPath) {
|
|
303
|
+
return commandPath === 'rpk cloud' || commandPath.startsWith('rpk cloud ') ||
|
|
304
|
+
commandPath === 'rpk security secret' || commandPath.startsWith('rpk security secret ')
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Antora component that publishes the rpk cloud and rpk security secret pages.
|
|
309
|
+
*/
|
|
310
|
+
const CLOUD_DOCS_COMPONENT = 'cloud-data-platform'
|
|
311
|
+
|
|
294
312
|
/**
|
|
295
313
|
* Get command metadata from overrides
|
|
296
314
|
* @param {Object} overrides - Overrides object (resolved)
|
|
@@ -2420,8 +2438,7 @@ function updateNavFile(navFile, commands, resolvedOverrides, topLevelWithSubcomm
|
|
|
2420
2438
|
if (isProtectedCommandPath(commandPath, protectedPlugins)) continue
|
|
2421
2439
|
if (shouldExcludeCommand(resolvedOverrides, commandPath)) continue
|
|
2422
2440
|
if (shouldUsePartialDir(resolvedOverrides, commandPath)) continue
|
|
2423
|
-
if (commandPath
|
|
2424
|
-
if (commandPath.startsWith('rpk security secret')) continue
|
|
2441
|
+
if (isCloudSecretCommand(commandPath)) continue
|
|
2425
2442
|
|
|
2426
2443
|
const parts = commandPath.split(' ')
|
|
2427
2444
|
const stars = '*'.repeat(parts.length + 1)
|
|
@@ -2725,16 +2742,13 @@ async function generateRpkDocs(options = {}) {
|
|
|
2725
2742
|
// Build subcommands with correct xref paths
|
|
2726
2743
|
// Filter out excluded and asPartial subcommands — excluded have no file,
|
|
2727
2744
|
// asPartial ones live in the partials directory with no linkable xref.
|
|
2728
|
-
// rpk cloud and rpk security secret
|
|
2729
|
-
//
|
|
2730
|
-
//
|
|
2731
|
-
//
|
|
2745
|
+
// rpk cloud and rpk security secret rows stay in the table: their pages
|
|
2746
|
+
// are routed to the cloud partials directory and published by
|
|
2747
|
+
// cloud-docs, so the xref below links across to the cloud component
|
|
2748
|
+
// instead of dropping the row (which hid the subcommand entirely).
|
|
2732
2749
|
const subcommands = (command.commands || [])
|
|
2733
2750
|
.filter(sub => {
|
|
2734
2751
|
const subPath = `${commandPath} ${sub.name}`
|
|
2735
|
-
if (subPath.startsWith('rpk cloud') || subPath.startsWith('rpk security secret')) {
|
|
2736
|
-
return false
|
|
2737
|
-
}
|
|
2738
2752
|
return !shouldExcludeCommand(resolvedOverrides, subPath) && !shouldUsePartialDir(resolvedOverrides, subPath)
|
|
2739
2753
|
})
|
|
2740
2754
|
.map(sub => {
|
|
@@ -2756,6 +2770,13 @@ async function generateRpkDocs(options = {}) {
|
|
|
2756
2770
|
}
|
|
2757
2771
|
}
|
|
2758
2772
|
|
|
2773
|
+
// rpk cloud and rpk security secret pages are routed to the cloud
|
|
2774
|
+
// partials directory and published by cloud-docs, so rows in regular
|
|
2775
|
+
// pages must link across to the cloud component.
|
|
2776
|
+
if (cloudSecretDir && isCloudSecretCommand(subPath) && !isCloudSecretCommand(commandPath)) {
|
|
2777
|
+
xrefPath = `${CLOUD_DOCS_COMPONENT}:${xrefPath}`
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2759
2780
|
// Use overridden description if available
|
|
2760
2781
|
const subOverride = resolvedOverrides?.commands?.[subPath]
|
|
2761
2782
|
const subDescription = subOverride?.description || sub.description || ''
|
|
@@ -2765,6 +2786,8 @@ async function generateRpkDocs(options = {}) {
|
|
|
2765
2786
|
fullPath: subPath,
|
|
2766
2787
|
dashifiedPath: dashifiedSubPath,
|
|
2767
2788
|
xrefPath,
|
|
2789
|
+
cloudOnly: subOverride?.cloudOnly === true,
|
|
2790
|
+
selfHostedOnly: subOverride?.selfHostedOnly === true,
|
|
2768
2791
|
shortDesc: ensurePeriod(capToTwoSentences(formatDescription(subDescription, textTransformations, { skipTableConversion: true, skipListConversion: true })))
|
|
2769
2792
|
}
|
|
2770
2793
|
})
|
|
@@ -2949,10 +2972,8 @@ async function generateRpkDocs(options = {}) {
|
|
|
2949
2972
|
|
|
2950
2973
|
// Determine output path
|
|
2951
2974
|
// Check if this command should go to cloudSecretDir
|
|
2952
|
-
const isCloudCommand = commandPath.startsWith('rpk cloud')
|
|
2953
|
-
const isSecuritySecretCommand = commandPath.startsWith('rpk security secret')
|
|
2954
2975
|
const useCloudSecretDir = cloudSecretDir && (
|
|
2955
|
-
|
|
2976
|
+
isCloudSecretCommand(commandPath) || shouldUsePartialDir(resolvedOverrides, commandPath)
|
|
2956
2977
|
)
|
|
2957
2978
|
const effectiveOutputDir = useCloudSecretDir ? cloudSecretDir : outputDir
|
|
2958
2979
|
|