@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.
- package/bin/doc-tools.js +105 -43
- package/cli-utils/enterprise-sources.js +244 -0
- package/cli-utils/helm-commented-values.js +361 -0
- package/cli-utils/self-managed-docs-branch.js +32 -24
- package/cli-utils/setup-mcp.js +4 -3
- package/mcp/USER_GUIDE.adoc +1 -1
- package/package.json +2 -1
- package/tools/enterprise-features/parsers.js +0 -14
- package/tools/enterprise-features/verify.js +28 -14
- 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
|
}
|
|
@@ -2130,7 +2223,7 @@ const validation = new Command('validate').description('Validate docs data again
|
|
|
2130
2223
|
* @why A feature must only be documented as enterprise under its approved
|
|
2131
2224
|
* external name. This check catches new license-gated features in core that
|
|
2132
2225
|
* have no registry entry yet, registry pointers that no longer match core,
|
|
2133
|
-
* and
|
|
2226
|
+
* and registry connect-plugin entries that no longer match info.csv.
|
|
2134
2227
|
*
|
|
2135
2228
|
* @example
|
|
2136
2229
|
* # Check everything against the default remote sources
|
|
@@ -2151,58 +2244,27 @@ validation
|
|
|
2151
2244
|
.description('Check the enterprise features registry against core, connect, and docs sources of truth')
|
|
2152
2245
|
.option('--registry <path>', 'Local path to enterprise-features.yml (default: fetch from docs repo main)')
|
|
2153
2246
|
.option('--tag <ref>', 'Redpanda git ref for the core headers', 'dev')
|
|
2154
|
-
.option('--connect-ref <ref>',
|
|
2247
|
+
.option('--connect-ref <ref>', "Connect git ref for info.csv ('latest' resolves the newest release tag; the registry documents released state)", 'latest')
|
|
2155
2248
|
.option('--docs-ref <ref>', 'Docs repo git ref for remote fetches', 'main')
|
|
2156
2249
|
.option('--disable-page <path>', 'Local path to disable-enterprise-features.adoc (default: fetch from docs repo)')
|
|
2157
|
-
.option('--antora <path>', 'Local path to rp-connect-docs antora.yml (default: fetch from rp-connect-docs main)')
|
|
2158
2250
|
.option('--properties <path>', 'Local path to a generated cluster-properties JSON; enables existence checks for gating properties')
|
|
2159
|
-
.option('--skip-connect', 'Skip the connect info.csv
|
|
2251
|
+
.option('--skip-connect', 'Skip the connect check (info.csv is not fetched, so registry connect-plugin entries go unverified)')
|
|
2160
2252
|
.option('--format <format>', 'Output format: text or json', 'text')
|
|
2161
2253
|
.option('--write-mapping <path>', 'Write the internal-to-external name mapping partial to this path')
|
|
2162
2254
|
.action(async (options) => {
|
|
2163
2255
|
const { runChecks, buildMappingPartial } = require('../tools/enterprise-features/verify')
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
const
|
|
2168
|
-
// Sources that fail to load are reported as error-level findings rather
|
|
2169
|
-
// than silently skipped, so CI cannot pass on a check that never ran.
|
|
2170
|
-
const failedSources = []
|
|
2171
|
-
async function fetchText (url, sourceName) {
|
|
2172
|
-
const resp = await fetch(url)
|
|
2173
|
-
if (!resp.ok) {
|
|
2174
|
-
if (sourceName) {
|
|
2175
|
-
failedSources.push({ level: 'error', check: 'fetch', message: `Could not load ${sourceName} (${url}): ${resp.status} ${resp.statusText}. The related checks did not run.` })
|
|
2176
|
-
return undefined
|
|
2177
|
-
}
|
|
2178
|
-
throw new Error(`Failed to fetch ${url}: ${resp.status} ${resp.statusText}`)
|
|
2179
|
-
}
|
|
2180
|
-
return resp.text()
|
|
2181
|
-
}
|
|
2182
|
-
const readLocal = (p) => fs.readFileSync(path.resolve(p), 'utf8')
|
|
2256
|
+
// Fetch wiring (GitHub auth, rejected-token retry, transient-failure
|
|
2257
|
+
// retries, --skip-connect short-circuits) lives in
|
|
2258
|
+
// cli-utils/enterprise-sources so it is testable without a network.
|
|
2259
|
+
const { loadEnterpriseSources } = require('../cli-utils/enterprise-sources')
|
|
2183
2260
|
|
|
2184
2261
|
try {
|
|
2185
|
-
const registryYaml =
|
|
2186
|
-
|
|
2187
|
-
: await fetchText(`${RAW}/redpanda-data/docs/${options.docsRef}/shared/modules/ROOT/partials/enterprise-features.yml`)
|
|
2188
|
-
const coreHeader = await fetchText(`${RAW}/redpanda-data/redpanda/${options.tag}/src/v/features/enterprise_features.h`, 'core enterprise_features.h')
|
|
2189
|
-
const configurationHeader = await fetchText(`${RAW}/redpanda-data/redpanda/${options.tag}/src/v/config/configuration.h`, 'core configuration.h')
|
|
2190
|
-
const infoCsv = options.skipConnect
|
|
2191
|
-
? undefined
|
|
2192
|
-
: await fetchText(`${RAW}/redpanda-data/connect/${options.connectRef}/internal/plugins/info.csv`, 'connect info.csv')
|
|
2193
|
-
const disablePage = options.disablePage
|
|
2194
|
-
? readLocal(options.disablePage)
|
|
2195
|
-
: await fetchText(`${RAW}/redpanda-data/docs/${options.docsRef}/modules/get-started/pages/licensing/disable-enterprise-features.adoc`, 'disable-enterprise-features.adoc')
|
|
2196
|
-
const antoraYaml = options.antora
|
|
2197
|
-
? readLocal(options.antora)
|
|
2198
|
-
: await fetchText(`${RAW}/redpanda-data/rp-connect-docs/main/antora.yml`, 'rp-connect-docs antora.yml')
|
|
2199
|
-
const antoraEnterpriseComponents = antoraYaml
|
|
2200
|
-
? extractAntoraEnterpriseComponents(yamlLib.load(antoraYaml))
|
|
2201
|
-
: undefined
|
|
2262
|
+
const { registryYaml, coreHeader, configurationHeader, infoCsv, connectRef, disablePage, failedSources } =
|
|
2263
|
+
await loadEnterpriseSources(options)
|
|
2202
2264
|
|
|
2203
2265
|
let allPropertyNames
|
|
2204
2266
|
if (options.properties) {
|
|
2205
|
-
const propertyData = JSON.parse(
|
|
2267
|
+
const propertyData = JSON.parse(fs.readFileSync(path.resolve(options.properties), 'utf8'))
|
|
2206
2268
|
allPropertyNames = Object.keys(propertyData.properties || propertyData)
|
|
2207
2269
|
}
|
|
2208
2270
|
|
|
@@ -2211,7 +2273,7 @@ validation
|
|
|
2211
2273
|
coreHeader,
|
|
2212
2274
|
configurationHeader,
|
|
2213
2275
|
infoCsv,
|
|
2214
|
-
|
|
2276
|
+
connectRef,
|
|
2215
2277
|
disablePage,
|
|
2216
2278
|
allPropertyNames,
|
|
2217
2279
|
})
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Source loading for `doc-tools validate enterprise-features`.
|
|
5
|
+
*
|
|
6
|
+
* Assembles the raw text of every source of truth the validator compares
|
|
7
|
+
* against: the enterprise features registry, the core headers, the connect
|
|
8
|
+
* plugin list, and the disable-enterprise-features page. Extracted from
|
|
9
|
+
* bin/doc-tools.js so the fetch wiring (auth, retries, --skip-connect
|
|
10
|
+
* short-circuits) is testable without a network.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs')
|
|
14
|
+
const path = require('path')
|
|
15
|
+
const { getGitHubToken } = require('./github-token')
|
|
16
|
+
|
|
17
|
+
const RAW = 'https://raw.githubusercontent.com'
|
|
18
|
+
|
|
19
|
+
// Repos known to be private, so a 404 can legitimately mean "needs auth".
|
|
20
|
+
// Public sources (redpanda core headers, connect info.csv) 404 only when the
|
|
21
|
+
// file is genuinely missing, and suggesting credentials there sends the
|
|
22
|
+
// reader after a problem that does not exist (e.g. during a transient
|
|
23
|
+
// GitHub blip).
|
|
24
|
+
const PRIVATE_REPO_PREFIXES = [
|
|
25
|
+
`${RAW}/redpanda-data/docs/`,
|
|
26
|
+
`${RAW}/redpanda-data/rp-connect-docs/`,
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
const TOKEN_HINT = ' This source is in a private repository: set GIT_CREDENTIALS (or GITHUB_TOKEN / REDPANDA_GITHUB_TOKEN / ACTIONS_BOT_TOKEN) so the fetch can authenticate.'
|
|
30
|
+
const REJECTED_HINT = ' A GitHub token was sent but rejected, so it may be expired or not grant access to this repository (the default Actions GITHUB_TOKEN is scoped to its own repository).'
|
|
31
|
+
|
|
32
|
+
const TRANSIENT_RETRIES = 3
|
|
33
|
+
const TRANSIENT_RETRY_DELAY_MS = 2000
|
|
34
|
+
|
|
35
|
+
function isPrivateSource (url) {
|
|
36
|
+
return PRIVATE_REPO_PREFIXES.some((prefix) => url.startsWith(prefix))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The response body of a failed fetch is never read; cancel it so the
|
|
40
|
+
// underlying connection is released instead of lingering until GC.
|
|
41
|
+
async function discardBody (resp) {
|
|
42
|
+
await resp.body?.cancel().catch(() => {})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build a fetchText function that authenticates raw.githubusercontent.com
|
|
47
|
+
* requests and records failures for named sources.
|
|
48
|
+
*
|
|
49
|
+
* Some sources live in private repos (the docs repo), which
|
|
50
|
+
* raw.githubusercontent serves as 404 rather than 401 when unauthenticated —
|
|
51
|
+
* indistinguishable from a genuinely missing file. A token is sent when one
|
|
52
|
+
* is available so those fetches resolve; public sources are unaffected.
|
|
53
|
+
*
|
|
54
|
+
* raw.githubusercontent also returns 404 (not 401) for a REJECTED token,
|
|
55
|
+
* even on public files, so a stale token in any of the token env vars would
|
|
56
|
+
* break fetches that work tokenless. On a 404 with a token, the fetch is
|
|
57
|
+
* retried once without authentication before the 404 is treated as real.
|
|
58
|
+
*
|
|
59
|
+
* Transient failures (network errors, 429, 5xx) are retried a few times
|
|
60
|
+
* with a delay — the same treatment the CGO test gives curl — because a
|
|
61
|
+
* single blip on any source would otherwise fail the whole run, and the
|
|
62
|
+
* check runs on a weekly cron where nobody is watching live.
|
|
63
|
+
*
|
|
64
|
+
* @param {object} [deps]
|
|
65
|
+
* @param {string|null} [deps.token] - GitHub token (defaults to getGitHubToken())
|
|
66
|
+
* @param {Function} [deps.fetchImpl] - fetch implementation (defaults to global fetch)
|
|
67
|
+
* @param {Function} [deps.warn] - warning sink (defaults to console.warn)
|
|
68
|
+
* @param {Function} [deps.sleep] - delay implementation, for tests
|
|
69
|
+
* @returns {{fetchText: Function, failedSources: Array}} fetchText and the
|
|
70
|
+
* findings array it appends to. Sources that fail to load are reported as
|
|
71
|
+
* error-level findings rather than silently skipped, so CI cannot pass on a
|
|
72
|
+
* check that never ran. Failures without a sourceName throw instead.
|
|
73
|
+
*/
|
|
74
|
+
function createSourceFetcher (deps = {}) {
|
|
75
|
+
const ghToken = deps.token !== undefined ? deps.token : getGitHubToken()
|
|
76
|
+
const fetchImpl = deps.fetchImpl || ((...args) => fetch(...args))
|
|
77
|
+
const warn = deps.warn || ((msg) => console.warn(msg))
|
|
78
|
+
const sleep = deps.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)))
|
|
79
|
+
const failedSources = []
|
|
80
|
+
let warnedRejectedToken = false
|
|
81
|
+
|
|
82
|
+
// One HTTP attempt with transient-failure retries. Returns the final
|
|
83
|
+
// response; network errors that persist through every retry are rethrown.
|
|
84
|
+
async function attemptFetch (url, opts) {
|
|
85
|
+
let lastError
|
|
86
|
+
for (let attempt = 1; attempt <= TRANSIENT_RETRIES; attempt++) {
|
|
87
|
+
let resp
|
|
88
|
+
try {
|
|
89
|
+
resp = await fetchImpl(url, opts)
|
|
90
|
+
} catch (err) {
|
|
91
|
+
lastError = err
|
|
92
|
+
if (attempt < TRANSIENT_RETRIES) await sleep(TRANSIENT_RETRY_DELAY_MS)
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
if (resp.status === 429 || resp.status >= 500) {
|
|
96
|
+
await discardBody(resp)
|
|
97
|
+
lastError = null
|
|
98
|
+
if (attempt < TRANSIENT_RETRIES) {
|
|
99
|
+
await sleep(TRANSIENT_RETRY_DELAY_MS)
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
return resp
|
|
103
|
+
}
|
|
104
|
+
return resp
|
|
105
|
+
}
|
|
106
|
+
throw lastError
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function fetchText (url, sourceName) {
|
|
110
|
+
let resp
|
|
111
|
+
try {
|
|
112
|
+
resp = await attemptFetch(url, ghToken ? { headers: { Authorization: `Bearer ${ghToken}` } } : undefined)
|
|
113
|
+
} catch (err) {
|
|
114
|
+
if (sourceName) {
|
|
115
|
+
failedSources.push({ level: 'error', check: 'fetch', message: `Could not load ${sourceName} (${url}): ${err.message}. The related checks did not run.` })
|
|
116
|
+
return undefined
|
|
117
|
+
}
|
|
118
|
+
throw new Error(`Failed to fetch ${url}: ${err.message}`)
|
|
119
|
+
}
|
|
120
|
+
if (resp.ok) return resp.text()
|
|
121
|
+
await discardBody(resp)
|
|
122
|
+
|
|
123
|
+
let hint = ''
|
|
124
|
+
if (resp.status === 404 && ghToken) {
|
|
125
|
+
const retry = await attemptFetch(url)
|
|
126
|
+
if (retry.ok) {
|
|
127
|
+
if (!warnedRejectedToken) {
|
|
128
|
+
warnedRejectedToken = true
|
|
129
|
+
warn('Warning: the configured GitHub token was rejected by raw.githubusercontent.com (it may be expired); fetching without authentication instead.')
|
|
130
|
+
}
|
|
131
|
+
return retry.text()
|
|
132
|
+
}
|
|
133
|
+
await discardBody(retry)
|
|
134
|
+
if (isPrivateSource(url)) hint = REJECTED_HINT
|
|
135
|
+
} else if (resp.status === 404 && isPrivateSource(url)) {
|
|
136
|
+
// A tokenless 404 on a known-private repo is the signature of an
|
|
137
|
+
// unauthenticated fetch, so say so instead of leaving it to look
|
|
138
|
+
// like a missing file. Public sources get no credential hint: their
|
|
139
|
+
// 404s mean the file is missing (or GitHub is having a bad moment).
|
|
140
|
+
hint = TOKEN_HINT
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (sourceName) {
|
|
144
|
+
failedSources.push({ level: 'error', check: 'fetch', message: `Could not load ${sourceName} (${url}): ${resp.status} ${resp.statusText}. The related checks did not run.${hint}` })
|
|
145
|
+
return undefined
|
|
146
|
+
}
|
|
147
|
+
throw new Error(`Failed to fetch ${url}: ${resp.status} ${resp.statusText}.${hint}`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { fetchText, failedSources }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Resolve the ref to compare connect plugins at.
|
|
155
|
+
*
|
|
156
|
+
* Primary: the newest published release tag from the GitHub API. Fallback
|
|
157
|
+
* when the API is unavailable: the `latest-connect-version` asciidoc
|
|
158
|
+
* attribute in rp-connect-docs antora.yml — the latest version the docs
|
|
159
|
+
* actually document, which is the state the registry describes. (This is a
|
|
160
|
+
* different use of that file than the retired enterprise-components list:
|
|
161
|
+
* the version attribute is actively maintained by the connect version-update
|
|
162
|
+
* automation.) If both routes fail, the connect check is skipped with an
|
|
163
|
+
* error finding rather than silently comparing against main.
|
|
164
|
+
*/
|
|
165
|
+
async function resolveLatestConnectRef ({ fetchText, failedSources, warn }) {
|
|
166
|
+
try {
|
|
167
|
+
const releaseJson = await fetchText('https://api.github.com/repos/redpanda-data/connect/releases/latest')
|
|
168
|
+
const tag = JSON.parse(releaseJson).tag_name
|
|
169
|
+
if (tag) return String(tag)
|
|
170
|
+
throw new Error('the GitHub API response has no tag_name')
|
|
171
|
+
} catch (err) {
|
|
172
|
+
warn(`Warning: could not resolve the latest connect release from the GitHub API (${err.message}); falling back to the latest documented connect version in rp-connect-docs antora.yml.`)
|
|
173
|
+
}
|
|
174
|
+
const antoraYaml = await fetchText(`${RAW}/redpanda-data/rp-connect-docs/main/antora.yml`, 'latest documented connect version (rp-connect-docs antora.yml fallback)')
|
|
175
|
+
if (antoraYaml === undefined) return undefined // fetch failure already reported
|
|
176
|
+
const yamlLib = require('js-yaml')
|
|
177
|
+
let documented
|
|
178
|
+
try {
|
|
179
|
+
const attributes = yamlLib.load(antoraYaml)?.asciidoc?.attributes
|
|
180
|
+
documented = attributes && attributes['latest-connect-version']
|
|
181
|
+
} catch (err) {
|
|
182
|
+
failedSources.push({ level: 'error', check: 'fetch', message: `Could not resolve the connect ref: rp-connect-docs antora.yml did not parse (${err.message}). The connect check did not run.` })
|
|
183
|
+
return undefined
|
|
184
|
+
}
|
|
185
|
+
if (!documented) {
|
|
186
|
+
failedSources.push({ level: 'error', check: 'fetch', message: 'Could not resolve the connect ref: rp-connect-docs antora.yml has no latest-connect-version attribute. The connect check did not run.' })
|
|
187
|
+
return undefined
|
|
188
|
+
}
|
|
189
|
+
const version = String(documented).trim()
|
|
190
|
+
return version.startsWith('v') ? version : `v${version}`
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Load every source the enterprise-features validation consumes.
|
|
195
|
+
*
|
|
196
|
+
* The rp-connect-docs antora.yml is deliberately NOT a source anymore:
|
|
197
|
+
* rp-connect-docs#485 removed the enterprise-components list it carried, so
|
|
198
|
+
* fetching it bought a cross-repo credential requirement in exchange for a
|
|
199
|
+
* comparison against nothing. The connect check now validates the registry's
|
|
200
|
+
* connect-plugin entries directly against info.csv.
|
|
201
|
+
*
|
|
202
|
+
* The connect ref defaults to 'latest', which resolves to the latest
|
|
203
|
+
* published connect release tag. The registry documents released state, so
|
|
204
|
+
* comparing it against main would flag plugins renamed or newly marked
|
|
205
|
+
* enterprise ahead of any release. The core headers deliberately stay on
|
|
206
|
+
* 'dev' for the opposite reason: their check exists to give early warning
|
|
207
|
+
* of NEW license-gated features before they ship.
|
|
208
|
+
*
|
|
209
|
+
* @param {object} options - Parsed CLI options: registry, tag, connectRef,
|
|
210
|
+
* docsRef, disablePage, skipConnect.
|
|
211
|
+
* @param {object} [deps] - Injectable dependencies for tests: token,
|
|
212
|
+
* fetchImpl, warn, sleep (see createSourceFetcher), and readLocal.
|
|
213
|
+
* @returns {Promise<object>} { registryYaml, coreHeader, configurationHeader,
|
|
214
|
+
* infoCsv, connectRef, disablePage, failedSources }
|
|
215
|
+
*/
|
|
216
|
+
async function loadEnterpriseSources (options, deps = {}) {
|
|
217
|
+
const { fetchText, failedSources } = createSourceFetcher(deps)
|
|
218
|
+
const readLocal = deps.readLocal || ((p) => fs.readFileSync(path.resolve(p), 'utf8'))
|
|
219
|
+
|
|
220
|
+
const registryYaml = options.registry
|
|
221
|
+
? readLocal(options.registry)
|
|
222
|
+
: await fetchText(`${RAW}/redpanda-data/docs/${options.docsRef}/shared/modules/ROOT/partials/enterprise-features.yml`)
|
|
223
|
+
const coreHeader = await fetchText(`${RAW}/redpanda-data/redpanda/${options.tag}/src/v/features/enterprise_features.h`, 'core enterprise_features.h')
|
|
224
|
+
const configurationHeader = await fetchText(`${RAW}/redpanda-data/redpanda/${options.tag}/src/v/config/configuration.h`, 'core configuration.h')
|
|
225
|
+
|
|
226
|
+
let connectRef = options.connectRef
|
|
227
|
+
let infoCsv
|
|
228
|
+
if (!options.skipConnect) {
|
|
229
|
+
if (!connectRef || connectRef === 'latest') {
|
|
230
|
+
connectRef = await resolveLatestConnectRef({ fetchText, failedSources, warn: deps.warn || ((msg) => console.warn(msg)) })
|
|
231
|
+
}
|
|
232
|
+
if (connectRef) {
|
|
233
|
+
infoCsv = await fetchText(`${RAW}/redpanda-data/connect/${connectRef}/internal/plugins/info.csv`, `connect info.csv (${connectRef})`)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const disablePage = options.disablePage
|
|
238
|
+
? readLocal(options.disablePage)
|
|
239
|
+
: await fetchText(`${RAW}/redpanda-data/docs/${options.docsRef}/modules/get-started/pages/licensing/disable-enterprise-features.adoc`, 'disable-enterprise-features.adoc')
|
|
240
|
+
|
|
241
|
+
return { registryYaml, coreHeader, configurationHeader, infoCsv, connectRef, disablePage, failedSources }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
module.exports = { createSourceFetcher, loadEnterpriseSources, RAW }
|