@redpanda-data/docs-extensions-and-macros 5.37.2 → 5.37.4
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/package.json
CHANGED
|
@@ -37,7 +37,7 @@ const fs = require('fs')
|
|
|
37
37
|
const os = require('os')
|
|
38
38
|
const path = require('path')
|
|
39
39
|
const { spawnSync } = require('child_process')
|
|
40
|
-
const { getRpUtilSchema, SCHEMA_FLAGS } = require('./rp-util-fetch')
|
|
40
|
+
const { getRpUtilSchema, schemaExpectation, fetchPublishedSchema, SCHEMA_FLAGS } = require('./rp-util-fetch')
|
|
41
41
|
const bigIntJson = require('../../cli-utils/big-int-json')
|
|
42
42
|
|
|
43
43
|
function getArg(args, flag) {
|
|
@@ -57,13 +57,140 @@ const RELEASE_TAG_RX = /^v?\d+\.\d+\.\d+(-rc\d+)?$/
|
|
|
57
57
|
* tag (see the header comment), a warning plus the Tree-sitter fallback for
|
|
58
58
|
* everything else.
|
|
59
59
|
*/
|
|
60
|
-
|
|
60
|
+
// How long to wait for a schema that is being built right now, and how often
|
|
61
|
+
// to look. streaming-enterprise's dispatch-docs-updates.yml fires the schema
|
|
62
|
+
// publish and this generation from the same job, and the publish is a
|
|
63
|
+
// ~27-minute Bazel build per scope, so on a fresh release tag the schema
|
|
64
|
+
// reliably does not exist yet when we first ask. The docs runner has neither
|
|
65
|
+
// bazel nor docker, so the from-source fallback cannot cover the gap: without
|
|
66
|
+
// waiting, every RC and GA regeneration fails on a race it is guaranteed to
|
|
67
|
+
// lose. Overridable so a one-off backfill need not sit through it.
|
|
68
|
+
/**
|
|
69
|
+
* Read a millisecond knob from the environment, or fall back. Number('') is 0
|
|
70
|
+
* and Number('abc') is NaN, and a 0 or NaN poll interval turns the wait loop
|
|
71
|
+
* into a busy-poll of the GitHub API for the whole budget. `allowZero` is for
|
|
72
|
+
* the wait budget, where 0 is the documented way to disable waiting.
|
|
73
|
+
*/
|
|
74
|
+
function parseEnvMs(name, fallback, { allowZero = false } = {}) {
|
|
75
|
+
const raw = process.env[name]
|
|
76
|
+
if (raw === undefined || raw === '') return fallback
|
|
77
|
+
const n = Number(raw)
|
|
78
|
+
if (Number.isInteger(n) && (n > 0 || (allowZero && n === 0))) return n
|
|
79
|
+
// Quoted by hand: this file is held to bigIntJson.stringify by a repo-wide
|
|
80
|
+
// guard test, and a plain string needs no BigInt-aware serializer anyway.
|
|
81
|
+
console.warn(`Warning: ${name}='${raw}' is not a ${allowZero ? 'non-negative' : 'positive'} integer; using ${fallback}.`)
|
|
82
|
+
return fallback
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const SCHEMA_WAIT_MS = parseEnvMs('RP_UTIL_SCHEMA_WAIT_MS', 45 * 60 * 1000, { allowZero: true })
|
|
86
|
+
const SCHEMA_POLL_MS = parseEnvMs('RP_UTIL_SCHEMA_POLL_MS', 60 * 1000)
|
|
87
|
+
const CLASSIFY_BACKOFF_MS = parseEnvMs('RP_UTIL_CLASSIFY_BACKOFF_MS', 5 * 1000)
|
|
88
|
+
|
|
89
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Poll for a published schema release until it appears or the budget runs out.
|
|
93
|
+
* @returns {Promise<object|null>} The schemas, or null if none appeared
|
|
94
|
+
*/
|
|
95
|
+
function isPermanentLookupError(err) {
|
|
96
|
+
const s = Number(err && err.status)
|
|
97
|
+
return s >= 400 && s < 500 && s !== 404 && s !== 429
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* schemaExpectation() with a few retries. It is one GitHub contents call, and
|
|
102
|
+
* a single flaky response used to leave the classification at 'unknown', which
|
|
103
|
+
* skipped the wait entirely and dropped a release straight into the race this
|
|
104
|
+
* module exists to avoid. Only exceptions are retried; a real classification,
|
|
105
|
+
* including 'unknown' from the classifier itself, is returned as-is.
|
|
106
|
+
*/
|
|
107
|
+
async function classifyWithRetry(releaseTag, { attempts = 3, backoffMs = CLASSIFY_BACKOFF_MS, sleepFn = sleep } = {}) {
|
|
108
|
+
let lastErr
|
|
109
|
+
for (let i = 1; i <= attempts; i++) {
|
|
110
|
+
try {
|
|
111
|
+
return await schemaExpectation(releaseTag)
|
|
112
|
+
} catch (err) {
|
|
113
|
+
lastErr = err
|
|
114
|
+
console.warn(`Warning: could not classify ${releaseTag} (attempt ${i} of ${attempts}): ${err.message}`)
|
|
115
|
+
if (i < attempts) await sleepFn(backoffMs * i)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { expectation: 'unknown', reason: `could not classify ${releaseTag} after ${attempts} attempts: ${lastErr.message}` }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function waitForPublishedSchema(releaseTag, budgetMs, pollMs, now = Date.now) {
|
|
122
|
+
const deadline = now() + budgetMs
|
|
123
|
+
let attempt = 0
|
|
124
|
+
for (;;) {
|
|
125
|
+
attempt++
|
|
126
|
+
let published = null
|
|
127
|
+
try {
|
|
128
|
+
published = await fetchPublishedSchema(releaseTag)
|
|
129
|
+
} catch (err) {
|
|
130
|
+
// A transient failure is not the same as "not published": log and
|
|
131
|
+
// keep waiting. A 4xx other than 404 (which fetchPublishedSchema maps
|
|
132
|
+
// to null) or 429 is permanent: a bad or under-scoped token will not
|
|
133
|
+
// heal in 45 minutes, so surface it now instead of after the budget.
|
|
134
|
+
if (isPermanentLookupError(err)) {
|
|
135
|
+
throw new Error(`Cannot look up the rp_util schema for ${releaseTag}: ${err.message}`)
|
|
136
|
+
}
|
|
137
|
+
console.warn(`Warning: schema lookup for ${releaseTag} failed (attempt ${attempt}): ${err.message}`)
|
|
138
|
+
}
|
|
139
|
+
if (published) {
|
|
140
|
+
console.log(`rp_util schema for ${releaseTag} is published (after ${attempt} check(s)).`)
|
|
141
|
+
return published
|
|
142
|
+
}
|
|
143
|
+
const remaining = deadline - now()
|
|
144
|
+
if (remaining <= 0) return null
|
|
145
|
+
const nap = Math.min(pollMs, remaining)
|
|
146
|
+
console.log(
|
|
147
|
+
`Waiting for the rp_util schema for ${releaseTag} to be published ` +
|
|
148
|
+
`(${Math.round(remaining / 1000)}s of budget left)...`
|
|
149
|
+
)
|
|
150
|
+
await sleep(nap)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function handleMergeUnavailable(tag, enhanced, reason, output = enhanced) {
|
|
61
155
|
if (RELEASE_TAG_RX.test(tag)) {
|
|
62
156
|
// Print the canonical v-prefixed tag in the remediation command:
|
|
63
157
|
// streaming-enterprise tags (and therefore schema release names) are
|
|
64
158
|
// always v-prefixed, so telling the operator to publish a v-less name
|
|
65
159
|
// would send them to a workflow run that can never help.
|
|
66
160
|
const releaseTag = tag.startsWith('v') ? tag : `v${tag}`
|
|
161
|
+
|
|
162
|
+
// Not every release CAN have a schema, and the ones that cannot do not
|
|
163
|
+
// need one. streaming-enterprise#63 added the broker-scope dump flags and
|
|
164
|
+
// the enum_set_property conversions in the same change, so a tag without
|
|
165
|
+
// the flags also has no enum_set properties for the Tree-sitter pass to
|
|
166
|
+
// misparse. Failing those releases forever would block every regeneration
|
|
167
|
+
// of a v26.2.x or older line on a schema that can never be published.
|
|
168
|
+
// See schemaExpectation() for the evidence.
|
|
169
|
+
const expectation = await classifyWithRetry(releaseTag)
|
|
170
|
+
|
|
171
|
+
if (expectation.expectation === 'unavailable') {
|
|
172
|
+
console.warn(`Warning: ${reason}`)
|
|
173
|
+
console.warn(`No rp_util schema can exist for ${releaseTag}: ${expectation.reason}`)
|
|
174
|
+
console.warn('Keeping the Tree-sitter-only extraction, which is accurate for this release.')
|
|
175
|
+
markRpUtilMergeUnavailable(enhanced)
|
|
176
|
+
// generate-docs reads --output right after this returns. When it is
|
|
177
|
+
// not the same file as --enhanced, the degraded result has to land
|
|
178
|
+
// there too, or the run exits 0 having left the output stale.
|
|
179
|
+
if (output && output !== enhanced) fs.copyFileSync(enhanced, output)
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (expectation.expectation === 'inconsistent') {
|
|
184
|
+
console.error(`Error: ${expectation.reason}`)
|
|
185
|
+
console.error(
|
|
186
|
+
'This is the one combination that must never ship: neither the schema ' +
|
|
187
|
+
'nor the Tree-sitter extraction can describe those properties. Backport ' +
|
|
188
|
+
'the rp_util schema flags to this branch before regenerating.'
|
|
189
|
+
)
|
|
190
|
+
process.exitCode = 1
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
|
|
67
194
|
console.error(`Error: ${reason}`)
|
|
68
195
|
console.error(
|
|
69
196
|
`Refusing to publish Tree-sitter-only property data for release ${tag}: ` +
|
|
@@ -138,14 +265,46 @@ async function main() {
|
|
|
138
265
|
|
|
139
266
|
console.log(`Fetching rp_util schema for ${tag}...`)
|
|
140
267
|
let schemas
|
|
268
|
+
|
|
269
|
+
// On a release tag whose rp_util can dump every scope, the schema is either
|
|
270
|
+
// already published or being built right now, so wait for it instead of
|
|
271
|
+
// racing the publisher. A tag that can never have one skips the wait and
|
|
272
|
+
// falls through to the classification in handleMergeUnavailable.
|
|
273
|
+
if (!sourcePath && RELEASE_TAG_RX.test(tag) && SCHEMA_WAIT_MS > 0) {
|
|
274
|
+
const releaseTag = tag.startsWith('v') ? tag : `v${tag}`
|
|
275
|
+
const expectation = await classifyWithRetry(releaseTag)
|
|
276
|
+
if (expectation.expectation === 'required') {
|
|
277
|
+
let waited
|
|
278
|
+
try {
|
|
279
|
+
waited = await waitForPublishedSchema(releaseTag, SCHEMA_WAIT_MS, SCHEMA_POLL_MS)
|
|
280
|
+
} catch (err) {
|
|
281
|
+
// 'required' means degrading is not an option, so a permanent
|
|
282
|
+
// lookup failure is the run's failure.
|
|
283
|
+
console.error(`Error: ${err.message}`)
|
|
284
|
+
process.exitCode = 1
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
if (waited) {
|
|
288
|
+
schemas = { ...waited, sourcePath: null }
|
|
289
|
+
} else {
|
|
290
|
+
console.warn(
|
|
291
|
+
`Warning: no rp_util schema for ${releaseTag} after waiting ` +
|
|
292
|
+
`${Math.round(SCHEMA_WAIT_MS / 60000)} minutes.`
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
141
298
|
try {
|
|
142
299
|
// --source-path builds from an existing local streaming-enterprise
|
|
143
300
|
// checkout instead of fetching a published release or cloning+building
|
|
144
301
|
// fresh -- same option rp-util-fetch.js itself exposes, useful for
|
|
145
302
|
// local iteration against a branch that has no published schema yet.
|
|
146
|
-
|
|
303
|
+
if (!schemas) {
|
|
304
|
+
schemas = await getRpUtilSchema(tag, sourcePath ? { sourcePath } : undefined)
|
|
305
|
+
}
|
|
147
306
|
} catch (err) {
|
|
148
|
-
handleMergeUnavailable(tag, enhanced, `could not get rp_util schema for ${tag}: ${err.message}
|
|
307
|
+
await handleMergeUnavailable(tag, enhanced, `could not get rp_util schema for ${tag}: ${err.message}`, output)
|
|
149
308
|
return
|
|
150
309
|
}
|
|
151
310
|
|
|
@@ -159,7 +318,7 @@ async function main() {
|
|
|
159
318
|
}
|
|
160
319
|
}
|
|
161
320
|
if (!anySchema) {
|
|
162
|
-
handleMergeUnavailable(tag, enhanced, `rp_util schema for ${tag} came back empty
|
|
321
|
+
await handleMergeUnavailable(tag, enhanced, `rp_util schema for ${tag} came back empty.`, output)
|
|
163
322
|
return
|
|
164
323
|
}
|
|
165
324
|
|
|
@@ -188,10 +347,11 @@ async function main() {
|
|
|
188
347
|
const result = spawnSync(pythonBin, mergeArgs, { cwd: __dirname, stdio: 'inherit' })
|
|
189
348
|
if (result.error || result.status !== 0) {
|
|
190
349
|
fs.rmSync(tempOutput, { force: true })
|
|
191
|
-
handleMergeUnavailable(
|
|
350
|
+
await handleMergeUnavailable(
|
|
192
351
|
tag,
|
|
193
352
|
enhanced,
|
|
194
|
-
`rp_util merge failed (${result.error ? result.error.message : `exit ${result.status}`})
|
|
353
|
+
`rp_util merge failed (${result.error ? result.error.message : `exit ${result.status}`}).`,
|
|
354
|
+
output
|
|
195
355
|
)
|
|
196
356
|
} else {
|
|
197
357
|
fs.renameSync(tempOutput, output)
|
|
@@ -201,7 +361,16 @@ async function main() {
|
|
|
201
361
|
}
|
|
202
362
|
}
|
|
203
363
|
|
|
204
|
-
module.exports = {
|
|
364
|
+
module.exports = {
|
|
365
|
+
main,
|
|
366
|
+
parseEnvMs,
|
|
367
|
+
classifyWithRetry,
|
|
368
|
+
isPermanentLookupError,
|
|
369
|
+
markRpUtilMergeUnavailable,
|
|
370
|
+
// exported for testing
|
|
371
|
+
waitForPublishedSchema,
|
|
372
|
+
handleMergeUnavailable
|
|
373
|
+
}
|
|
205
374
|
|
|
206
375
|
if (require.main === module) {
|
|
207
376
|
main()
|
|
@@ -1032,6 +1032,85 @@ def report_phantom_stubs():
|
|
|
1032
1032
|
logger.warning("=" * 70)
|
|
1033
1033
|
|
|
1034
1034
|
|
|
1035
|
+
def infer_topic_property_category(name):
|
|
1036
|
+
"""Map a topic property's dotted name to a category, for properties with
|
|
1037
|
+
no explicit category override.
|
|
1038
|
+
|
|
1039
|
+
"other" is the fallback for a name that matches no group. It only renders
|
|
1040
|
+
where a page includes tags=category-other: cloud-docs' topic properties
|
|
1041
|
+
page does (cloud-docs#686); docs' topic-properties.adoc and
|
|
1042
|
+
broker-properties.adoc do not yet, so on docs main a property that falls
|
|
1043
|
+
to "other" is present in the partial but appears on no page. The point of
|
|
1044
|
+
the fallback is that such a property is at least reachable by a known tag,
|
|
1045
|
+
rather than untagged and unreachable by any include.
|
|
1046
|
+
"""
|
|
1047
|
+
retention = [
|
|
1048
|
+
"cleanup.policy", "compaction.strategy", "delete.retention.ms", "max.compaction.lag.ms",
|
|
1049
|
+
"min.cleanable.dirty.ratio", "min.compaction.lag.ms", "retention.bytes", "retention.ms"
|
|
1050
|
+
]
|
|
1051
|
+
segment = [
|
|
1052
|
+
"compression.type", "max.message.bytes", "message.timestamp.type", "segment.bytes", "segment.ms"
|
|
1053
|
+
]
|
|
1054
|
+
performance = [
|
|
1055
|
+
"flush.bytes", "flush.ms", "redpanda.leaders.preference", "replication.factor", "write.caching"
|
|
1056
|
+
]
|
|
1057
|
+
tiered = [
|
|
1058
|
+
"initial.retention.local.target.bytes", "initial.retention.local.target.ms", "redpanda.remote.delete",
|
|
1059
|
+
"redpanda.remote.read", "redpanda.remote.recovery", "redpanda.remote.write", "retention.local.target.bytes",
|
|
1060
|
+
"retention.local.target.ms"
|
|
1061
|
+
]
|
|
1062
|
+
remote_replica = ["redpanda.remote.readreplica"]
|
|
1063
|
+
iceberg = [
|
|
1064
|
+
"redpanda.iceberg.delete", "redpanda.iceberg.invalid.record.action", "redpanda.iceberg.mode",
|
|
1065
|
+
"redpanda.iceberg.partition.spec", "redpanda.iceberg.target.lag.ms"
|
|
1066
|
+
]
|
|
1067
|
+
schema_registry = [
|
|
1068
|
+
"redpanda.key.schema.id.validation", "redpanda.key.subject.name.strategy", "redpanda.value.schema.id.validation",
|
|
1069
|
+
"redpanda.value.subject.name.strategy", "confluent.key.schema.validation", "confluent.key.subject.name.strategy",
|
|
1070
|
+
"confluent.value.schema.validation", "confluent.value.subject.name.strategy"
|
|
1071
|
+
]
|
|
1072
|
+
if name in retention:
|
|
1073
|
+
return "retention-compaction"
|
|
1074
|
+
if name in segment:
|
|
1075
|
+
return "segment-message"
|
|
1076
|
+
if name in performance:
|
|
1077
|
+
return "performance-cluster"
|
|
1078
|
+
if name in tiered:
|
|
1079
|
+
return "tiered-storage"
|
|
1080
|
+
if name in remote_replica:
|
|
1081
|
+
return "remote-read-replica"
|
|
1082
|
+
if name in iceberg:
|
|
1083
|
+
return "iceberg-integration"
|
|
1084
|
+
if name in schema_registry:
|
|
1085
|
+
return "schema-registry"
|
|
1086
|
+
return "other"
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
def _ensure_category_fallback(properties):
|
|
1090
|
+
"""Guarantee every property has a category, so the property.hbs/
|
|
1091
|
+
topic-property.hbs templates' `{{#if category}}` always wraps it in a
|
|
1092
|
+
tag::category-X[] region. A property with no category at all gets no
|
|
1093
|
+
tag region and is silently excluded by every page that assembles its
|
|
1094
|
+
content from specific category-tag includes -- present in the raw
|
|
1095
|
+
generated partial, invisible everywhere it's actually read from.
|
|
1096
|
+
|
|
1097
|
+
Runs after overrides are applied (including override-created phantom
|
|
1098
|
+
stub properties, which previously never received a category at all)
|
|
1099
|
+
so this covers every property regardless of where it came from.
|
|
1100
|
+
"""
|
|
1101
|
+
for prop_name, property_data in properties.items():
|
|
1102
|
+
if not hasattr(property_data, "get") or property_data.get("category"):
|
|
1103
|
+
continue
|
|
1104
|
+
if property_data.get("config_scope") == "topic" or property_data.get("is_topic_property"):
|
|
1105
|
+
property_data["category"] = infer_topic_property_category(property_data.get("name", prop_name))
|
|
1106
|
+
else:
|
|
1107
|
+
# No per-name taxonomy exists for cluster/broker properties today
|
|
1108
|
+
# (unlike topic properties' infer_topic_property_category) --
|
|
1109
|
+
# "other" at least guarantees a real, included tag region
|
|
1110
|
+
# instead of silent exclusion.
|
|
1111
|
+
property_data["category"] = "other"
|
|
1112
|
+
|
|
1113
|
+
|
|
1035
1114
|
def apply_property_overrides(properties, overrides, overrides_file_path=None):
|
|
1036
1115
|
"""
|
|
1037
1116
|
Apply overrides from an overrides mapping to the extracted properties, mutating and returning the properties dictionary.
|
|
@@ -1114,6 +1193,8 @@ def apply_property_overrides(properties, overrides, overrides_file_path=None):
|
|
|
1114
1193
|
for property_data in properties.values():
|
|
1115
1194
|
if hasattr(property_data, "get") and property_data.get("description"):
|
|
1116
1195
|
property_data["description"] = _normalize_config_ref_macros(property_data["description"])
|
|
1196
|
+
|
|
1197
|
+
_ensure_category_fallback(properties)
|
|
1117
1198
|
return properties
|
|
1118
1199
|
|
|
1119
1200
|
|
|
@@ -2844,49 +2925,6 @@ def extract_topic_properties(source_path, cluster_properties=None):
|
|
|
2844
2925
|
if prop_data.get("is_noop", False):
|
|
2845
2926
|
continue
|
|
2846
2927
|
|
|
2847
|
-
# Assign category based on property name pattern or mapping
|
|
2848
|
-
def infer_category(name):
|
|
2849
|
-
retention = [
|
|
2850
|
-
"cleanup.policy", "compaction.strategy", "delete.retention.ms", "max.compaction.lag.ms",
|
|
2851
|
-
"min.cleanable.dirty.ratio", "min.compaction.lag.ms", "retention.bytes", "retention.ms"
|
|
2852
|
-
]
|
|
2853
|
-
segment = [
|
|
2854
|
-
"compression.type", "max.message.bytes", "message.timestamp.type", "segment.bytes", "segment.ms"
|
|
2855
|
-
]
|
|
2856
|
-
performance = [
|
|
2857
|
-
"flush.bytes", "flush.ms", "redpanda.leaders.preference", "replication.factor", "write.caching"
|
|
2858
|
-
]
|
|
2859
|
-
tiered = [
|
|
2860
|
-
"initial.retention.local.target.bytes", "initial.retention.local.target.ms", "redpanda.remote.delete",
|
|
2861
|
-
"redpanda.remote.read", "redpanda.remote.recovery", "redpanda.remote.write", "retention.local.target.bytes",
|
|
2862
|
-
"retention.local.target.ms"
|
|
2863
|
-
]
|
|
2864
|
-
remote_replica = ["redpanda.remote.readreplica"]
|
|
2865
|
-
iceberg = [
|
|
2866
|
-
"redpanda.iceberg.delete", "redpanda.iceberg.invalid.record.action", "redpanda.iceberg.mode",
|
|
2867
|
-
"redpanda.iceberg.partition.spec", "redpanda.iceberg.target.lag.ms"
|
|
2868
|
-
]
|
|
2869
|
-
schema_registry = [
|
|
2870
|
-
"redpanda.key.schema.id.validation", "redpanda.key.subject.name.strategy", "redpanda.value.schema.id.validation",
|
|
2871
|
-
"redpanda.value.subject.name.strategy", "confluent.key.schema.validation", "confluent.key.subject.name.strategy",
|
|
2872
|
-
"confluent.value.schema.validation", "confluent.value.subject.name.strategy"
|
|
2873
|
-
]
|
|
2874
|
-
if name in retention:
|
|
2875
|
-
return "retention-compaction"
|
|
2876
|
-
if name in segment:
|
|
2877
|
-
return "segment-message"
|
|
2878
|
-
if name in performance:
|
|
2879
|
-
return "performance-cluster"
|
|
2880
|
-
if name in tiered:
|
|
2881
|
-
return "tiered-storage"
|
|
2882
|
-
if name in remote_replica:
|
|
2883
|
-
return "remote-read-replica"
|
|
2884
|
-
if name in iceberg:
|
|
2885
|
-
return "iceberg-integration"
|
|
2886
|
-
if name in schema_registry:
|
|
2887
|
-
return "schema-registry"
|
|
2888
|
-
return "other"
|
|
2889
|
-
|
|
2890
2928
|
converted_properties[prop_name] = {
|
|
2891
2929
|
"name": prop_name,
|
|
2892
2930
|
"description": prop_data.get("description", ""),
|
|
@@ -2900,7 +2938,7 @@ def extract_topic_properties(source_path, cluster_properties=None):
|
|
|
2900
2938
|
"acceptable_values": prop_data.get("acceptable_values", ""),
|
|
2901
2939
|
"is_deprecated": False,
|
|
2902
2940
|
"is_topic_property": True,
|
|
2903
|
-
"category":
|
|
2941
|
+
"category": infer_topic_property_category(prop_name)
|
|
2904
2942
|
}
|
|
2905
2943
|
|
|
2906
2944
|
# Add default values if they exist (inherited from cluster properties)
|
|
@@ -276,6 +276,94 @@ const RELEASE_ASSET_TO_SCHEMA_KEY = {
|
|
|
276
276
|
'schema-registry-config-schema.json': 'schemaRegistrySchema'
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
// Files schemaExpectation() reads to decide whether a ref can have a schema.
|
|
280
|
+
// Both were changed by the same PR (streaming-enterprise#63), which is what
|
|
281
|
+
// makes the pairing in schemaExpectation() a property of the source rather
|
|
282
|
+
// than a guess.
|
|
283
|
+
const SE_SOURCE_REPO = 'redpanda-data/streaming-enterprise'
|
|
284
|
+
const RP_UTIL_MAIN_PATH = 'src/v/rp_util/main.cc'
|
|
285
|
+
const CONFIGURATION_H_PATH = 'src/v/config/configuration.h'
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Read one file from streaming-enterprise at a ref.
|
|
289
|
+
* @returns {Promise<string|null>} Contents, or null if it cannot be read
|
|
290
|
+
*/
|
|
291
|
+
async function fetchSourceFile(ref, filePath) {
|
|
292
|
+
const token = getGitHubApiToken()
|
|
293
|
+
const headers = { Accept: 'application/vnd.github.raw' }
|
|
294
|
+
if (token) headers.Authorization = `Bearer ${token}`
|
|
295
|
+
const resp = await fetch(
|
|
296
|
+
`https://api.github.com/repos/${SE_SOURCE_REPO}/contents/${filePath}?ref=${encodeURIComponent(ref)}`,
|
|
297
|
+
{ headers }
|
|
298
|
+
)
|
|
299
|
+
if (!resp.ok) return null
|
|
300
|
+
return resp.text()
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Whether a ref can produce an rp_util schema, and whether it needs one.
|
|
305
|
+
*
|
|
306
|
+
* These two are the same question, because they arrived in the same commit.
|
|
307
|
+
* streaming-enterprise#63 added both the four broker-scope dump flags to
|
|
308
|
+
* rp_util and the `enum_set_property` conversions that the Tree-sitter
|
|
309
|
+
* extractor misparses. So a ref either has both or neither:
|
|
310
|
+
*
|
|
311
|
+
* ref enum_set_property broker-scope flags
|
|
312
|
+
* v26.2.1 0 0/4
|
|
313
|
+
* v26.2.2 0 0/4
|
|
314
|
+
* v26.2.3-rc1 0 0/4
|
|
315
|
+
* v26.2.x (branch) 3 4/4
|
|
316
|
+
* dev 3 4/4
|
|
317
|
+
*
|
|
318
|
+
* That is what makes degrading safe for an old tag: a tag with no flags can
|
|
319
|
+
* never have a schema, and equally has no enum_set properties to misparse, so
|
|
320
|
+
* the Tree-sitter extraction is correct for it rather than merely tolerable.
|
|
321
|
+
*
|
|
322
|
+
* The flags are matched QUOTED. `"config_schema_json"` must not match inside
|
|
323
|
+
* `"node_config_schema_json"`, and the leading quote is what prevents it.
|
|
324
|
+
*
|
|
325
|
+
* @param {string} ref - Tag, branch, or SHA
|
|
326
|
+
* @returns {Promise<{expectation: string, reason: string}>} expectation is one
|
|
327
|
+
* of 'required' (schema needed and obtainable), 'unavailable' (this ref can
|
|
328
|
+
* never have one, and does not need one), 'inconsistent' (needs one but
|
|
329
|
+
* cannot have one, which must never ship), or 'unknown' (could not tell)
|
|
330
|
+
*/
|
|
331
|
+
async function schemaExpectation(ref) {
|
|
332
|
+
const mainCc = await fetchSourceFile(ref, RP_UTIL_MAIN_PATH)
|
|
333
|
+
if (mainCc === null) {
|
|
334
|
+
return { expectation: 'unknown', reason: `could not read ${RP_UTIL_MAIN_PATH} at ${ref}` }
|
|
335
|
+
}
|
|
336
|
+
const missingFlags = SCHEMA_FLAGS
|
|
337
|
+
.map(({ flag }) => flag.replace(/^--/, ''))
|
|
338
|
+
.filter((name) => !mainCc.includes(`"${name}"`))
|
|
339
|
+
|
|
340
|
+
if (missingFlags.length === 0) {
|
|
341
|
+
return { expectation: 'required', reason: `rp_util at ${ref} dumps every scope` }
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// No flags. Confirm the other half of the pairing rather than assuming it:
|
|
345
|
+
// if this ref somehow carries enum_set properties without the dump flags,
|
|
346
|
+
// the Tree-sitter fallback would silently drop them and degrading would be
|
|
347
|
+
// the wrong call.
|
|
348
|
+
const configurationH = await fetchSourceFile(ref, CONFIGURATION_H_PATH)
|
|
349
|
+
if (configurationH === null) {
|
|
350
|
+
return {
|
|
351
|
+
expectation: 'unknown',
|
|
352
|
+
reason: `rp_util at ${ref} cannot dump ${missingFlags.join(', ')}, and ${CONFIGURATION_H_PATH} could not be read to confirm it has no enum_set properties`
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (configurationH.includes('enum_set_property')) {
|
|
356
|
+
return {
|
|
357
|
+
expectation: 'inconsistent',
|
|
358
|
+
reason: `${ref} declares enum_set properties but its rp_util cannot dump ${missingFlags.join(', ')}, so neither the schema nor the Tree-sitter extraction can describe them correctly`
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
expectation: 'unavailable',
|
|
363
|
+
reason: `rp_util at ${ref} predates the broker-scope dumps (${missingFlags.join(', ')}) and ${ref} declares no enum_set properties, so the Tree-sitter extraction is correct for it`
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
279
367
|
/**
|
|
280
368
|
* Fetch rp_util's schema from a previously published GitHub release
|
|
281
369
|
* (publish-rp-util-schema.yaml's rp-util-schema-<tag> release in this repo)
|
|
@@ -296,10 +384,10 @@ async function fetchPublishedSchema(tag) {
|
|
|
296
384
|
)
|
|
297
385
|
if (releaseResp.status === 404) return null
|
|
298
386
|
if (!releaseResp.ok) {
|
|
299
|
-
throw new Error(
|
|
387
|
+
throw Object.assign(new Error(
|
|
300
388
|
`Failed to look up published rp_util schema release for ${tag}: ` +
|
|
301
389
|
`${releaseResp.status} ${releaseResp.statusText}`
|
|
302
|
-
)
|
|
390
|
+
), { status: releaseResp.status })
|
|
303
391
|
}
|
|
304
392
|
const release = await releaseResp.json()
|
|
305
393
|
|
|
@@ -333,9 +421,9 @@ async function fetchPublishedSchema(tag) {
|
|
|
333
421
|
headers: { ...headers, Accept: 'application/octet-stream' }
|
|
334
422
|
})
|
|
335
423
|
if (!assetResp.ok) {
|
|
336
|
-
throw new Error(
|
|
424
|
+
throw Object.assign(new Error(
|
|
337
425
|
`Failed to download published rp_util schema asset ${asset.name}: ${assetResp.status}`
|
|
338
|
-
)
|
|
426
|
+
), { status: assetResp.status })
|
|
339
427
|
}
|
|
340
428
|
let payload
|
|
341
429
|
try {
|
|
@@ -437,6 +525,7 @@ async function getRpUtilSchema(ref, options = {}) {
|
|
|
437
525
|
|
|
438
526
|
module.exports = {
|
|
439
527
|
getRpUtilSchema,
|
|
528
|
+
schemaExpectation,
|
|
440
529
|
SCHEMA_FLAGS,
|
|
441
530
|
// exported for testing
|
|
442
531
|
cloneStreamingEnterprise,
|