@redpanda-data/docs-extensions-and-macros 5.29.0 → 5.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/doc-tools.js +54 -19
- package/bin/mcp-tools/versions.js +1 -1
- package/cli-utils/enterprise-sources.js +6 -6
- package/cli-utils/install-test-dependencies.sh +58 -6
- package/extensions/version-fetcher/set-latest-version.js +11 -2
- package/mcp/CLI_INTERFACE.adoc +1 -1
- package/mcp/USER_GUIDE.adoc +4 -4
- package/package.json +1 -1
- package/tools/bundle-openapi.js +28 -10
- package/tools/get-redpanda-version.js +14 -1
- package/tools/property-extractor/Makefile +99 -42
- package/tools/property-extractor/README.adoc +1 -1
- package/tools/property-extractor/cloud_config.py +11 -7
- package/tools/property-extractor/merge-rp-util.js +208 -0
- package/tools/property-extractor/rp-util-fetch.js +447 -0
- package/tools/property-extractor/rp_util_merge.py +599 -0
- package/tools/property-extractor/templates/property.hbs +12 -5
- package/tools/property-extractor/templates/topic-property.hbs +7 -4
- package/tools/rpk-docs/rpk-docs-handler.js +98 -16
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Maps rp_util's JSON schema output into property_extractor.py's existing
|
|
3
|
+
pre-override property-dict shape, then splices the result into an already-
|
|
4
|
+
generated enhanced-output file (the same {"properties": ..., "definitions":
|
|
5
|
+
...} shape --enhanced-output writes).
|
|
6
|
+
|
|
7
|
+
rp_util (see tools/property-extractor/rp-util-fetch.js) gives us default,
|
|
8
|
+
bounds, and enterprise-restriction data already resolved at runtime --
|
|
9
|
+
unlike property_extractor.py's C++-source parsing, which infers the same
|
|
10
|
+
facts by pattern-matching constructor call shapes (see EnterpriseTransformer
|
|
11
|
+
in transformers.py). This module never re-runs that inference for the
|
|
12
|
+
properties rp_util covers; it derives the equivalent fields directly from
|
|
13
|
+
rp_util's fields instead. Topic properties, and anything rp_util doesn't
|
|
14
|
+
cover, pass through the existing --enhanced-output file untouched.
|
|
15
|
+
|
|
16
|
+
Integration point: this splices into the FINAL enhanced-output, not into
|
|
17
|
+
property_extractor.py's raw pre-resolution properties dict. That raw dict is
|
|
18
|
+
still an intermediate, C++-source-shaped representation (unresolved type
|
|
19
|
+
references, unexpanded chrono expressions, numeric enum defaults) that
|
|
20
|
+
resolve_type_and_default/map_enum_defaults/evaluate_chrono_expressions exist
|
|
21
|
+
specifically to resolve -- rp_util's data is already in the final, resolved
|
|
22
|
+
shape those functions produce, so it is spliced in after them, not fed
|
|
23
|
+
through them.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import logging
|
|
28
|
+
import os
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
# rp_util's defined_in string for each schema, keyed by the SCHEMA_FLAGS keys
|
|
33
|
+
# rp-util-fetch.js's getRpUtilSchema() returns. Matches the exact literal
|
|
34
|
+
# strings add_config_scope() in property_extractor.py already keys on.
|
|
35
|
+
DEFINED_IN_BY_SCHEMA_KEY = {
|
|
36
|
+
"clusterSchema": "src/v/config/configuration.cc",
|
|
37
|
+
"nodeSchema": "src/v/config/node_config.cc",
|
|
38
|
+
"pandaproxySchema": "src/v/pandaproxy/rest/configuration.cc",
|
|
39
|
+
"kafkaClientSchema": "src/v/kafka/client/configuration.cc",
|
|
40
|
+
"schemaRegistrySchema": "src/v/pandaproxy/schema_registry/configuration.cc",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
CONFIG_SCOPE_BY_SCHEMA_KEY = {
|
|
44
|
+
"clusterSchema": "cluster",
|
|
45
|
+
"nodeSchema": "broker",
|
|
46
|
+
"pandaproxySchema": "broker",
|
|
47
|
+
"kafkaClientSchema": "broker",
|
|
48
|
+
"schemaRegistrySchema": "broker",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# rp_util's raw "units" value is always this bare abbreviation (verified
|
|
52
|
+
# across all cluster+broker properties); property.hbs/topic-property.hbs
|
|
53
|
+
# render it via {{capitalize units}}, so passing the abbreviation straight
|
|
54
|
+
# through renders "Ms"/"S" instead of the full word the docs actually want
|
|
55
|
+
# -- the same vocabulary helpers/formatUnits.js already uses for its
|
|
56
|
+
# property-name-suffix fallback.
|
|
57
|
+
UNIT_ABBREVIATION_TO_WORD = {"ms": "milliseconds", "s": "seconds"}
|
|
58
|
+
|
|
59
|
+
JSON_SCHEMA_PRIMITIVES = {"string", "integer", "number", "boolean", "object", "array"}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _parse_embedded_json(value, property_name, field_name):
|
|
63
|
+
"""rp_util embeds default/bounds/enterprise values as JSON-encoded
|
|
64
|
+
strings (see cluster_config_property_metadata's default_value/minimum/
|
|
65
|
+
maximum/enterprise_sanctioned_value in api-doc/cluster_config.json) --
|
|
66
|
+
a string containing valid JSON, not the value itself."""
|
|
67
|
+
if value is None:
|
|
68
|
+
return None
|
|
69
|
+
try:
|
|
70
|
+
return json.loads(value)
|
|
71
|
+
except (TypeError, ValueError) as exc:
|
|
72
|
+
logger.warning(
|
|
73
|
+
"%s: could not parse rp_util's %s (%r) as JSON: %s",
|
|
74
|
+
property_name, field_name, value, exc,
|
|
75
|
+
)
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _build_enum_metadata(enum_values, enterprise_restricted_values):
|
|
80
|
+
"""Map rp_util's enterprise_restricted_values (the subset of enum_values
|
|
81
|
+
that requires an enterprise license) into baseline's x-enum-metadata
|
|
82
|
+
shape: every enum value mapped to {"is_enterprise": bool}. Returns None
|
|
83
|
+
when rp_util reports no enterprise-restricted values at all, so a
|
|
84
|
+
property with a plain (non-enterprise) enum never gets a vacuous
|
|
85
|
+
all-false x-enum-metadata block baseline never produced either.
|
|
86
|
+
"""
|
|
87
|
+
if not enum_values or not enterprise_restricted_values:
|
|
88
|
+
return None
|
|
89
|
+
enterprise_set = set(enterprise_restricted_values)
|
|
90
|
+
return {v: {"is_enterprise": v in enterprise_set} for v in enum_values}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _derive_enterprise_fields(meta, default_value, property_name):
|
|
94
|
+
"""Recreate is_enterprise/enterprise_constructor/enterprise_value/
|
|
95
|
+
enterprise_restricted_value/enterprise_sanctioned_value (this module's
|
|
96
|
+
own field names, not rp_util's) -- the fields
|
|
97
|
+
EnterpriseTransformer (transformers.py) infers from C++ constructor call
|
|
98
|
+
shapes -- directly from rp_util's ground truth instead.
|
|
99
|
+
|
|
100
|
+
config::enterprise<P> has exactly two constructors: one that takes only
|
|
101
|
+
a restriction (the sanctioned value then defaults to the property's own
|
|
102
|
+
default value), and one that also takes an explicit sanctioned value.
|
|
103
|
+
rp_util's enterprise_sanctioned_value is therefore always present when
|
|
104
|
+
is_enterprise is true -- whether it differs from the property's own
|
|
105
|
+
default is what distinguishes those two constructors here, since there
|
|
106
|
+
is no separate marker for it (unlike the C++-source pattern matcher's
|
|
107
|
+
"restricted_with_sanctioned" vs. "restricted_only" cases, which come
|
|
108
|
+
from counting constructor arguments).
|
|
109
|
+
"""
|
|
110
|
+
is_enterprise = bool(meta.get("is_enterprise", False))
|
|
111
|
+
if not is_enterprise:
|
|
112
|
+
return {"is_enterprise": False}
|
|
113
|
+
|
|
114
|
+
fields = {"is_enterprise": True}
|
|
115
|
+
|
|
116
|
+
if meta.get("enterprise_restriction_is_dynamic"):
|
|
117
|
+
# The restriction is a predicate function, not a static value or
|
|
118
|
+
# list -- nothing to enumerate.
|
|
119
|
+
fields["enterprise_constructor"] = "simple"
|
|
120
|
+
return fields
|
|
121
|
+
|
|
122
|
+
# Unlike enterprise_sanctioned_value below, enterprise_restricted_values
|
|
123
|
+
# is a real JSON array in rp_util's output, not a JSON-encoded string --
|
|
124
|
+
# one item per restricted value, already in the shape this module wants.
|
|
125
|
+
restricted_list = meta.get("enterprise_restricted_values") or []
|
|
126
|
+
sanctioned = _parse_embedded_json(
|
|
127
|
+
meta.get("enterprise_sanctioned_value"), property_name,
|
|
128
|
+
"enterprise_sanctioned_value",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if sanctioned == default_value:
|
|
132
|
+
fields["enterprise_constructor"] = "restricted_only"
|
|
133
|
+
fields["enterprise_restricted_value"] = restricted_list
|
|
134
|
+
fields["enterprise_value"] = restricted_list # backward compat, see transformers.py
|
|
135
|
+
else:
|
|
136
|
+
fields["enterprise_constructor"] = "restricted_with_sanctioned"
|
|
137
|
+
fields["enterprise_restricted_value"] = restricted_list
|
|
138
|
+
fields["enterprise_sanctioned_value"] = (
|
|
139
|
+
sanctioned if isinstance(sanctioned, list) else [sanctioned]
|
|
140
|
+
)
|
|
141
|
+
return fields
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _resolve_type(raw_type, has_enum_values, definitions):
|
|
145
|
+
"""Resolve a raw C++ type/class name from rp_util into the JSON-schema
|
|
146
|
+
type name property.hbs actually renders.
|
|
147
|
+
|
|
148
|
+
rp_util's own "type" field is a JSON-schema primitive only for scalars;
|
|
149
|
+
for anything else it's the literal C++ class/typedef name (e.g.
|
|
150
|
+
"net::unresolved_address", "tls_config", "leaders_preference"), and
|
|
151
|
+
property.hbs renders `type` literally -- an unmapped pass-through leaks
|
|
152
|
+
C++ internals straight into the published Type column. rp_util's naming
|
|
153
|
+
isn't consistently bare or namespace-qualified (e.g. "leaders_preference"
|
|
154
|
+
is bare where baseline's definitions dict key is
|
|
155
|
+
"config::leaders_preference", but "net::unresolved_address" already
|
|
156
|
+
carries its own prefix), so try bare, then the model:: and config::
|
|
157
|
+
prefixes baseline's definitions dict actually uses.
|
|
158
|
+
"""
|
|
159
|
+
if raw_type in JSON_SCHEMA_PRIMITIVES:
|
|
160
|
+
return raw_type
|
|
161
|
+
|
|
162
|
+
for candidate in (raw_type, f"model::{raw_type}", f"config::{raw_type}"):
|
|
163
|
+
entry = definitions.get(candidate)
|
|
164
|
+
if entry:
|
|
165
|
+
resolved = entry.get("type", "object")
|
|
166
|
+
# baseline renders enum-backed properties as `string` (enum) in
|
|
167
|
+
# the Type column, never as a bare "enum" -- see property.hbs.
|
|
168
|
+
return "string" if resolved == "enum" else resolved
|
|
169
|
+
|
|
170
|
+
# Not found: baseline's definitions dict is known-incomplete (confirmed
|
|
171
|
+
# missing tls_config, throughput_control_group, ...) -- fall back the
|
|
172
|
+
# same way baseline effectively does for an unrecognized struct type.
|
|
173
|
+
return "string" if has_enum_values else "object"
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def map_rp_util_property(name, meta, config_scope, defined_in, definitions):
|
|
177
|
+
"""Map one rp_util property entry (cluster_config_property_metadata)
|
|
178
|
+
into property_extractor.py's property-dict shape."""
|
|
179
|
+
default_value = _parse_embedded_json(meta.get("default_value"), name, "default_value")
|
|
180
|
+
has_enum_values = bool(meta.get("enum_values"))
|
|
181
|
+
|
|
182
|
+
# Baseline consistently unwraps a single-element list default to its
|
|
183
|
+
# bare element when that element is a plain scalar (confirmed for
|
|
184
|
+
# sasl_mechanisms) -- formatPropertyValue.js renders a bare scalar list
|
|
185
|
+
# literally (e.g. "[SCRAM]"), so left as-is this is a real, if minor,
|
|
186
|
+
# rendered-output regression relative to today's docs, not just a
|
|
187
|
+
# JSON-shape difference. The property's own "type" still correctly says
|
|
188
|
+
# "array" regardless, so no information is lost. Only the *displayed*
|
|
189
|
+
# default is unwrapped -- _derive_enterprise_fields below still gets the
|
|
190
|
+
# original shape, since its enterprise_sanctioned_value == default_value
|
|
191
|
+
# comparison needs to match rp_util's own (unwrapped) val_container_t
|
|
192
|
+
# serialization.
|
|
193
|
+
#
|
|
194
|
+
# This must NOT apply when the single element is an object (confirmed
|
|
195
|
+
# for admin/kafka_api/pandaproxy_api/schema_registry_api): unwrapping
|
|
196
|
+
# there strips the array away before formatPropertyValue.js ever sees
|
|
197
|
+
# it, so its correct array-of-object branch never runs and its object
|
|
198
|
+
# branch renders a bracket-less `{name: "", address: ..., port: ...}`
|
|
199
|
+
# instead -- contradicting the property's own "array" type and not
|
|
200
|
+
# something a reader could paste back into a real array-typed config.
|
|
201
|
+
display_default = default_value
|
|
202
|
+
if (
|
|
203
|
+
isinstance(display_default, list)
|
|
204
|
+
and len(display_default) == 1
|
|
205
|
+
and not isinstance(display_default[0], dict)
|
|
206
|
+
):
|
|
207
|
+
display_default = display_default[0]
|
|
208
|
+
|
|
209
|
+
prop = {
|
|
210
|
+
"name": name,
|
|
211
|
+
"config_scope": config_scope,
|
|
212
|
+
"defined_in": defined_in,
|
|
213
|
+
"description": meta.get("description") or "",
|
|
214
|
+
"type": _resolve_type(meta.get("type"), has_enum_values, definitions),
|
|
215
|
+
"nullable": bool(meta.get("nullable", False)),
|
|
216
|
+
"needs_restart": bool(meta.get("needs_restart", False)),
|
|
217
|
+
"visibility": meta.get("visibility"),
|
|
218
|
+
"is_secret": bool(meta.get("is_secret", False)),
|
|
219
|
+
# rp_util's for_each() already excludes is_hidden() properties
|
|
220
|
+
# (see config_schema_util.cc), so anything reaching here
|
|
221
|
+
# is, by construction, not deprecated-and-hidden.
|
|
222
|
+
"is_deprecated": False,
|
|
223
|
+
"default": display_default,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
# enterprise_restricted_values is never nested under items -- it
|
|
227
|
+
# describes the whole property's license gating the same way
|
|
228
|
+
# is_enterprise/enterprise_sanctioned_value do, whether the property is
|
|
229
|
+
# a scalar or an array, so both the scalar and per-element branches
|
|
230
|
+
# below read it from the top level of meta.
|
|
231
|
+
enterprise_restricted_values = meta.get("enterprise_restricted_values")
|
|
232
|
+
if has_enum_values:
|
|
233
|
+
prop["enum"] = meta["enum_values"]
|
|
234
|
+
metadata = _build_enum_metadata(meta["enum_values"], enterprise_restricted_values)
|
|
235
|
+
if metadata:
|
|
236
|
+
prop["x-enum-metadata"] = metadata
|
|
237
|
+
if meta.get("items"):
|
|
238
|
+
items = dict(meta["items"])
|
|
239
|
+
item_enum_values = items.pop("enum_values", None)
|
|
240
|
+
if items.get("type"):
|
|
241
|
+
items["type"] = _resolve_type(items["type"], bool(item_enum_values), definitions)
|
|
242
|
+
if item_enum_values:
|
|
243
|
+
# rp_util reports an array property's per-element accepted
|
|
244
|
+
# values under items.enum_values -- baseline (and property.hbs)
|
|
245
|
+
# expect them nested the same way, but keyed "enum" (matching
|
|
246
|
+
# the top-level convention: "enum_values" only because swagger
|
|
247
|
+
# forbids the reserved word "enum" as a generated struct member).
|
|
248
|
+
items["enum"] = item_enum_values
|
|
249
|
+
metadata = _build_enum_metadata(item_enum_values, enterprise_restricted_values)
|
|
250
|
+
if metadata:
|
|
251
|
+
items["x-enum-metadata"] = metadata
|
|
252
|
+
prop["items"] = items
|
|
253
|
+
if meta.get("example") is not None:
|
|
254
|
+
# Matches ExampleTransformer (transformers.py): wrap in backticks for
|
|
255
|
+
# inline code formatting -- property.hbs renders {{{example}}} as
|
|
256
|
+
# raw HTML, so the backticks are the only thing producing that
|
|
257
|
+
# styling. Without this, a passed-through example loses its
|
|
258
|
+
# code-block appearance relative to every other property's.
|
|
259
|
+
prop["example"] = f"`{meta['example']}`"
|
|
260
|
+
units = meta.get("units")
|
|
261
|
+
if units is not None:
|
|
262
|
+
# Expand rp_util's bare abbreviation to the full word
|
|
263
|
+
# property.hbs's {{capitalize units}} expects -- see
|
|
264
|
+
# UNIT_ABBREVIATION_TO_WORD.
|
|
265
|
+
prop["units"] = UNIT_ABBREVIATION_TO_WORD.get(units, units)
|
|
266
|
+
if meta.get("aliases"):
|
|
267
|
+
prop["aliases"] = meta["aliases"]
|
|
268
|
+
if "gets_restored" in meta:
|
|
269
|
+
prop["gets_restored"] = bool(meta["gets_restored"])
|
|
270
|
+
|
|
271
|
+
minimum = _parse_embedded_json(meta.get("minimum"), name, "minimum")
|
|
272
|
+
if minimum is not None:
|
|
273
|
+
prop["minimum"] = minimum
|
|
274
|
+
maximum = _parse_embedded_json(meta.get("maximum"), name, "maximum")
|
|
275
|
+
if maximum is not None:
|
|
276
|
+
prop["maximum"] = maximum
|
|
277
|
+
|
|
278
|
+
if units in UNIT_ABBREVIATION_TO_WORD and isinstance(display_default, (int, float)):
|
|
279
|
+
# Mirrors property_extractor.py's evaluate_chrono_expressions, which
|
|
280
|
+
# computes this from just (value, unit) -- verified to reproduce
|
|
281
|
+
# baseline's value exactly for every ms/s property whose default
|
|
282
|
+
# isn't itself wrong (see the two evaluator bugs this migration
|
|
283
|
+
# already found and fixed independently of this field). bool is an
|
|
284
|
+
# int subclass in Python -- isinstance(display_default, (int, float))
|
|
285
|
+
# would wrongly accept a boolean default, but no ms/s-unit property
|
|
286
|
+
# has a boolean default, so this is a non-issue in practice.
|
|
287
|
+
from property_extractor import format_time_human_readable
|
|
288
|
+
prop["default_human_readable"] = format_time_human_readable(display_default, units)
|
|
289
|
+
|
|
290
|
+
prop.update(_derive_enterprise_fields(meta, default_value, name))
|
|
291
|
+
return prop
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def map_schema(schema_json, schema_key, definitions):
|
|
295
|
+
"""Map one rp_util schema blob into a {name: property_dict} dict.
|
|
296
|
+
schema_key is one of DEFINED_IN_BY_SCHEMA_KEY's keys."""
|
|
297
|
+
config_scope = CONFIG_SCOPE_BY_SCHEMA_KEY[schema_key]
|
|
298
|
+
defined_in = DEFINED_IN_BY_SCHEMA_KEY[schema_key]
|
|
299
|
+
properties = (schema_json or {}).get("properties", {})
|
|
300
|
+
return {
|
|
301
|
+
name: map_rp_util_property(name, meta, config_scope, defined_in, definitions)
|
|
302
|
+
for name, meta in properties.items()
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def map_rp_util_schemas(schemas, definitions=None):
|
|
307
|
+
"""Map every schema in `schemas` (the dict rp-util-fetch.js's
|
|
308
|
+
getRpUtilSchema() returns) into one combined {name: property_dict} dict
|
|
309
|
+
covering all of cluster and broker scope.
|
|
310
|
+
@param definitions: the existing --enhanced-output's "definitions" dict,
|
|
311
|
+
used to resolve non-primitive C++ type names (see _resolve_type).
|
|
312
|
+
"""
|
|
313
|
+
combined = {}
|
|
314
|
+
for schema_key in DEFINED_IN_BY_SCHEMA_KEY:
|
|
315
|
+
if schema_key in schemas:
|
|
316
|
+
combined.update(map_schema(schemas[schema_key], schema_key, definitions or {}))
|
|
317
|
+
return combined
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _preserve_validator_derived_enum_data(prop, existing_prop):
|
|
321
|
+
"""FALLBACK, not the primary path (mirrors _carry_forward_gets_restored):
|
|
322
|
+
rp_util now exposes accepted-value/enterprise-tier data for properties
|
|
323
|
+
like sasl_mechanisms directly (enum_set_property's enum_values(), plus
|
|
324
|
+
enterprise<P>'s enterprise_restricted_values(), see streaming-enterprise's
|
|
325
|
+
property.h) instead of it living only in a validator function's source, which
|
|
326
|
+
ValidatorEnumExtractor (transformers.py) used to have to parse. This
|
|
327
|
+
only fires against an rp_util build from before that existed, as a
|
|
328
|
+
stopgap so a stale rp_util binary doesn't regress published docs by
|
|
329
|
+
silently dropping real, verified, licensing-relevant content that
|
|
330
|
+
baseline already has. Preserve baseline's value whenever rp_util's
|
|
331
|
+
entry has none -- the same pass-through treatment topic properties
|
|
332
|
+
already get. Should be removed once every consumer reliably runs an
|
|
333
|
+
rp_util build new enough to report this itself.
|
|
334
|
+
"""
|
|
335
|
+
if not existing_prop:
|
|
336
|
+
return
|
|
337
|
+
if not prop.get("enum") and existing_prop.get("enum"):
|
|
338
|
+
prop["enum"] = existing_prop["enum"]
|
|
339
|
+
if not prop.get("x-enum-metadata") and existing_prop.get("x-enum-metadata"):
|
|
340
|
+
prop["x-enum-metadata"] = existing_prop["x-enum-metadata"]
|
|
341
|
+
if isinstance(prop.get("items"), dict) and isinstance(existing_prop.get("items"), dict):
|
|
342
|
+
# Array-typed properties (e.g. sasl_mechanisms) carry their enum --
|
|
343
|
+
# and, critically, its enterprise-tier x-enum-metadata flags -- under
|
|
344
|
+
# items, not at the top level.
|
|
345
|
+
if not prop["items"].get("enum") and existing_prop["items"].get("enum"):
|
|
346
|
+
prop["items"]["enum"] = existing_prop["items"]["enum"]
|
|
347
|
+
if not prop["items"].get("x-enum-metadata") and existing_prop["items"].get("x-enum-metadata"):
|
|
348
|
+
prop["items"]["x-enum-metadata"] = existing_prop["items"]["x-enum-metadata"]
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _carry_forward_gets_restored(prop, existing_prop):
|
|
352
|
+
"""FALLBACK, not the primary path: rp_util now exposes gets_restored
|
|
353
|
+
directly (map_rp_util_property sets it from meta["gets_restored"] when
|
|
354
|
+
present -- see streaming-enterprise's config_schema_util.cc).
|
|
355
|
+
This only fires against an rp_util build from before that field existed
|
|
356
|
+
(confirmed absent by enumerating every key across all five schemas as of
|
|
357
|
+
PR #63's original state), as a stopgap so a stale rp_util binary doesn't
|
|
358
|
+
actively regress published docs in the meantime: property.hbs/
|
|
359
|
+
topic-property.hbs render a missing gets_restored as "Yes" via
|
|
360
|
+
{{#if (ne gets_restored false)}}, silently inverting the correct "No"
|
|
361
|
+
for properties like cloud_storage_access_key. This fallback is itself
|
|
362
|
+
imperfect -- fragile for a newly-introduced property (never seen in a
|
|
363
|
+
baseline run) and silently stale if a property's real flag value ever
|
|
364
|
+
changes upstream -- so it should be removed once every consumer is
|
|
365
|
+
reliably running an rp_util build that includes the field.
|
|
366
|
+
"""
|
|
367
|
+
if "gets_restored" in prop:
|
|
368
|
+
return
|
|
369
|
+
if existing_prop and "gets_restored" in existing_prop:
|
|
370
|
+
prop["gets_restored"] = existing_prop["gets_restored"]
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _carry_forward_example(prop, existing_prop):
|
|
374
|
+
"""rp_util's own `example` field (config::base_property::example(), a
|
|
375
|
+
plain C++ string literal) only exists for a property whose author wrote
|
|
376
|
+
one there -- unlike enum/gets_restored data, there is no equivalent
|
|
377
|
+
override or dynamic source to fall back to, and no expectation that
|
|
378
|
+
every property should have one. But a property that already had a
|
|
379
|
+
correct, hand-written multi-line example in the existing output (e.g.
|
|
380
|
+
ExampleOverrideTransformer-applied content, or one baseline's own
|
|
381
|
+
parser found) must not silently lose it just because rp_util itself
|
|
382
|
+
reports none for that property. Preserve baseline's value whenever
|
|
383
|
+
rp_util's entry has none -- the same pass-through treatment topic
|
|
384
|
+
properties already get. Unlike the other two fallbacks above, this is
|
|
385
|
+
not a stopgap for a stale rp_util binary: rp_util is not expected to
|
|
386
|
+
ever report every property's example, so this keeps applying
|
|
387
|
+
indefinitely.
|
|
388
|
+
"""
|
|
389
|
+
if "example" in prop:
|
|
390
|
+
return
|
|
391
|
+
if existing_prop and "example" in existing_prop:
|
|
392
|
+
prop["example"] = existing_prop["example"]
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
CLOUD_METADATA_FIELDS = ("cloud_editable", "cloud_readonly", "cloud_supported", "cloud_byoc_only")
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _carry_forward_cloud_metadata(prop, existing_prop):
|
|
399
|
+
"""add_cloud_support_metadata (cloud_config.py) is the only source of
|
|
400
|
+
cloud_editable/cloud_readonly/cloud_supported/cloud_byoc_only, and it
|
|
401
|
+
only runs when merge_with_existing_output is given a CloudConfig (the
|
|
402
|
+
--cloud-support flag). Without one, a rp_util-covered property's
|
|
403
|
+
replaced entry has none of these four fields, silently erasing them for
|
|
404
|
+
any property that had real cloud metadata from an earlier
|
|
405
|
+
--cloud-support run -- until the next run happens to pass one again.
|
|
406
|
+
Preserve the existing values whenever they're missing, the same
|
|
407
|
+
fallback treatment gets_restored/example already get. A no-op whenever
|
|
408
|
+
add_cloud_support_metadata did run, since it always sets all four.
|
|
409
|
+
"""
|
|
410
|
+
if not existing_prop:
|
|
411
|
+
return
|
|
412
|
+
for field in CLOUD_METADATA_FIELDS:
|
|
413
|
+
if field not in prop and field in existing_prop:
|
|
414
|
+
prop[field] = existing_prop[field]
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _resync_topic_properties_inherited_from_cluster(merged_properties):
|
|
418
|
+
"""topic_property_extractor.py (property_extractor.py's Tree-sitter pass)
|
|
419
|
+
copies `default`/`default_human_readable`/`type` from a topic property's
|
|
420
|
+
corresponding_cluster_property onto the topic property itself, at
|
|
421
|
+
extraction time -- before this module ever runs. Once rp_util has
|
|
422
|
+
corrected that same cluster property's default/type here, the topic
|
|
423
|
+
property's own copy of it is stale (confirmed real: log_segment_ms's
|
|
424
|
+
default was the human string "2 weeks" in the old extraction and the
|
|
425
|
+
correct raw integer via rp_util; a topic property that inherited the
|
|
426
|
+
stale one would keep showing "2 weeks" as its own default forever,
|
|
427
|
+
since nothing re-copies it after this point otherwise). Mirror that
|
|
428
|
+
same copy here, now that the source it copies from is corrected.
|
|
429
|
+
"""
|
|
430
|
+
for prop in merged_properties.values():
|
|
431
|
+
cluster_prop_name = prop.get("corresponding_cluster_property")
|
|
432
|
+
if not cluster_prop_name:
|
|
433
|
+
continue
|
|
434
|
+
cluster_prop = merged_properties.get(cluster_prop_name)
|
|
435
|
+
if not cluster_prop:
|
|
436
|
+
continue
|
|
437
|
+
if "default" in cluster_prop:
|
|
438
|
+
prop["default"] = cluster_prop["default"]
|
|
439
|
+
if "default_human_readable" in cluster_prop:
|
|
440
|
+
prop["default_human_readable"] = cluster_prop["default_human_readable"]
|
|
441
|
+
elif "default_human_readable" in prop:
|
|
442
|
+
del prop["default_human_readable"]
|
|
443
|
+
if "type" in cluster_prop:
|
|
444
|
+
prop["type"] = cluster_prop["type"]
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def merge_with_existing_output(
|
|
448
|
+
existing_properties_and_definitions, rp_util_schemas, overrides,
|
|
449
|
+
overrides_file_path=None, cloud_config=None,
|
|
450
|
+
):
|
|
451
|
+
"""
|
|
452
|
+
Replace every cluster- and broker-scope property in an existing
|
|
453
|
+
enhanced-output dict (property_extractor.py's --enhanced-output shape)
|
|
454
|
+
with rp_util-derived data, running the same override-application (and,
|
|
455
|
+
if given, cloud metadata) steps used for every other property so both
|
|
456
|
+
sources produce identically-shaped output. Topic properties -- and
|
|
457
|
+
anything rp_util doesn't cover -- pass through untouched.
|
|
458
|
+
|
|
459
|
+
@param existing_properties_and_definitions: {"properties": {...}, "definitions": {...}}
|
|
460
|
+
@param rp_util_schemas: dict as returned by rp-util-fetch.js's getRpUtilSchema()
|
|
461
|
+
@param overrides: parsed docs-data/property-overrides.json contents
|
|
462
|
+
@param cloud_config: an already-fetched cloud_config.CloudConfig, or None to skip
|
|
463
|
+
cloud metadata (mirrors property_extractor.py's own --cloud-support flag)
|
|
464
|
+
"""
|
|
465
|
+
# Imported lazily: property_extractor.py has heavy import-time side
|
|
466
|
+
# effects (tree-sitter setup) that only matter when this module runs as
|
|
467
|
+
# part of the full pipeline invoking it, not for its own unit tests.
|
|
468
|
+
from property_extractor import apply_property_overrides
|
|
469
|
+
|
|
470
|
+
existing_properties = existing_properties_and_definitions.get("properties", {})
|
|
471
|
+
raw = map_rp_util_schemas(rp_util_schemas, existing_properties_and_definitions.get("definitions"))
|
|
472
|
+
|
|
473
|
+
# apply_property_overrides treats any override key with no matching
|
|
474
|
+
# entry in `properties` as a removed/renamed property and fabricates a
|
|
475
|
+
# phantom stub for it -- appropriate when `properties` is the full
|
|
476
|
+
# extraction, but `raw` here is only cluster+broker scope. Left
|
|
477
|
+
# unfiltered, every topic-only override key (the large majority of
|
|
478
|
+
# docs-data/property-overrides.json) would fabricate a stub, which then
|
|
479
|
+
# clobbers that topic property's real, correctly-extracted entry when
|
|
480
|
+
# merged back into the existing output below.
|
|
481
|
+
scoped_overrides = dict(overrides)
|
|
482
|
+
scoped_overrides["properties"] = {
|
|
483
|
+
name: override for name, override in overrides.get("properties", {}).items()
|
|
484
|
+
if name in raw
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
enhanced = apply_property_overrides(raw, scoped_overrides, overrides_file_path)
|
|
488
|
+
|
|
489
|
+
for name, prop in enhanced.items():
|
|
490
|
+
_preserve_validator_derived_enum_data(prop, existing_properties.get(name))
|
|
491
|
+
_carry_forward_gets_restored(prop, existing_properties.get(name))
|
|
492
|
+
_carry_forward_example(prop, existing_properties.get(name))
|
|
493
|
+
_carry_forward_cloud_metadata(prop, existing_properties.get(name))
|
|
494
|
+
|
|
495
|
+
merged_properties = dict(existing_properties)
|
|
496
|
+
replaced = sum(1 for name in enhanced if name in merged_properties)
|
|
497
|
+
added = len(enhanced) - replaced
|
|
498
|
+
merged_properties.update(enhanced)
|
|
499
|
+
|
|
500
|
+
# Annotated after the merge, over the whole property map rather than over
|
|
501
|
+
# the rp_util slice alone, so that every cluster property in the output
|
|
502
|
+
# has been through the same CloudConfig no matter which pass produced it.
|
|
503
|
+
# add_cloud_support_metadata touches cluster-scope entries only and
|
|
504
|
+
# always sets all four fields, so re-running it over the properties
|
|
505
|
+
# property_extractor.py already annotated is idempotent.
|
|
506
|
+
#
|
|
507
|
+
# What actually gets this metadata onto a property rp_util reports but
|
|
508
|
+
# the Tree-sitter pass never saw (real case: post-#63 Tree-sitter drops
|
|
509
|
+
# http_authentication and rp_util restores it) is main() building a
|
|
510
|
+
# CloudConfig for --cloud-support/CLOUD_SUPPORT=1. Without one there is
|
|
511
|
+
# nothing to annotate with, and such a property has no existing entry for
|
|
512
|
+
# _carry_forward_cloud_metadata to copy from either, so it lands with no
|
|
513
|
+
# cloud_supported at all -- which drops it from the Cloud reference's
|
|
514
|
+
# redpanda-cloud tagged include.
|
|
515
|
+
if cloud_config is not None:
|
|
516
|
+
from cloud_config import add_cloud_support_metadata
|
|
517
|
+
add_cloud_support_metadata(merged_properties, cloud_config)
|
|
518
|
+
|
|
519
|
+
_resync_topic_properties_inherited_from_cluster(merged_properties)
|
|
520
|
+
|
|
521
|
+
logger.info(
|
|
522
|
+
"rp_util merge: replaced %d existing propert%s, added %d new from rp_util",
|
|
523
|
+
replaced, "y" if replaced == 1 else "ies", added,
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
"properties": merged_properties,
|
|
528
|
+
"definitions": existing_properties_and_definitions.get("definitions", {}),
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _load_json(path):
|
|
533
|
+
with open(path, encoding="utf-8") as f:
|
|
534
|
+
return json.load(f)
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def main():
|
|
538
|
+
import argparse
|
|
539
|
+
|
|
540
|
+
parser = argparse.ArgumentParser(
|
|
541
|
+
description="Merge rp_util schema JSON into an existing property_extractor "
|
|
542
|
+
"--enhanced-output file."
|
|
543
|
+
)
|
|
544
|
+
parser.add_argument("--enhanced", required=True,
|
|
545
|
+
help="Path to the existing --enhanced-output JSON file")
|
|
546
|
+
parser.add_argument("--rp-util-dir", required=True,
|
|
547
|
+
help="Directory containing <schemaKey>.json files, "
|
|
548
|
+
"one per rp-util-fetch.js SCHEMA_FLAGS key")
|
|
549
|
+
parser.add_argument("--overrides", help="Path to property-overrides.json")
|
|
550
|
+
parser.add_argument("--output", required=True, help="Where to write the merged JSON")
|
|
551
|
+
parser.add_argument("--cloud-support", action="store_true",
|
|
552
|
+
help="Fetch Cloud metadata from the cloudv2 install pack and annotate "
|
|
553
|
+
"cluster properties with it (requires GITHUB_TOKEN). Mirrors "
|
|
554
|
+
"property_extractor.py's flag of the same name; without it, a "
|
|
555
|
+
"property rp_util adds that the Tree-sitter pass missed gets no "
|
|
556
|
+
"cloud metadata at all.")
|
|
557
|
+
args = parser.parse_args()
|
|
558
|
+
|
|
559
|
+
existing = _load_json(args.enhanced)
|
|
560
|
+
|
|
561
|
+
schemas = {}
|
|
562
|
+
for schema_key in DEFINED_IN_BY_SCHEMA_KEY:
|
|
563
|
+
schema_path = os.path.join(args.rp_util_dir, f"{schema_key}.json")
|
|
564
|
+
if os.path.exists(schema_path):
|
|
565
|
+
schemas[schema_key] = _load_json(schema_path)
|
|
566
|
+
else:
|
|
567
|
+
logger.warning("No %s found at %s; that scope keeps its existing (non-rp_util) data",
|
|
568
|
+
schema_key, schema_path)
|
|
569
|
+
|
|
570
|
+
overrides = _load_json(args.overrides) if args.overrides and os.path.exists(args.overrides) else {}
|
|
571
|
+
|
|
572
|
+
# Flag or CLOUD_SUPPORT=1, the same pair property_extractor.py honors
|
|
573
|
+
# (see its own --cloud-support handling), so a doc-tools run that asked
|
|
574
|
+
# for cloud metadata still gets it here even when the flag itself isn't
|
|
575
|
+
# threaded all the way through merge-rp-util.js.
|
|
576
|
+
cloud_config = None
|
|
577
|
+
if args.cloud_support or os.environ.get("CLOUD_SUPPORT") == "1":
|
|
578
|
+
from cloud_config import fetch_cloud_config
|
|
579
|
+
# Deliberately allowed to raise, exactly as property_extractor.py's
|
|
580
|
+
# own --cloud-support path does: writing cluster properties with no
|
|
581
|
+
# cloud metadata is the outcome this flag exists to prevent, so it
|
|
582
|
+
# must not degrade quietly into a run that looks successful.
|
|
583
|
+
cloud_config = fetch_cloud_config()
|
|
584
|
+
logger.info(
|
|
585
|
+
"Cloud support enabled; annotating with install-pack configuration version %s",
|
|
586
|
+
cloud_config.version,
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
merged = merge_with_existing_output(
|
|
590
|
+
existing, schemas, overrides, args.overrides, cloud_config
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
with open(args.output, "w", encoding="utf-8") as f:
|
|
594
|
+
json.dump(merged, f, indent=4, sort_keys=True)
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
if __name__ == "__main__":
|
|
598
|
+
logging.basicConfig(level=logging.INFO)
|
|
599
|
+
main()
|
|
@@ -48,7 +48,7 @@ ifndef::env-cloud[]
|
|
|
48
48
|
====
|
|
49
49
|
{{#if enterprise_constructor}}
|
|
50
50
|
{{#if (eq enterprise_constructor "simple")}}
|
|
51
|
-
|
|
51
|
+
Some values of this property require an Enterprise license.
|
|
52
52
|
{{else if (eq enterprise_constructor "restricted_only")}}
|
|
53
53
|
The following values require an Enterprise license: {{#each enterprise_restricted_value}}`{{this}}`{{#unless @last}}, {{/unless}}{{/each}}
|
|
54
54
|
{{else if (eq enterprise_constructor "restricted_with_sanctioned")}}
|
|
@@ -99,17 +99,17 @@ ifdef::env-cloud[]
|
|
|
99
99
|
{{#each items.enum}}`{{this}}`{{#unless @last}}, {{/unless}}{{/each}}
|
|
100
100
|
endif::[]
|
|
101
101
|
{{/if}}{{/if}}
|
|
102
|
-
{{#if (and minimum maximum)}}
|
|
102
|
+
{{#if (and (ne minimum undefined) (ne maximum undefined))}}
|
|
103
103
|
|
|
104
104
|
| Range
|
|
105
105
|
| [`{{minimum}}`, `{{maximum}}`]
|
|
106
106
|
{{else}}
|
|
107
|
-
{{#if minimum}}
|
|
107
|
+
{{#if (ne minimum undefined)}}
|
|
108
108
|
|
|
109
109
|
| Minimum
|
|
110
110
|
| `{{minimum}}`
|
|
111
111
|
{{/if}}
|
|
112
|
-
{{#if maximum}}
|
|
112
|
+
{{#if (ne maximum undefined)}}
|
|
113
113
|
|
|
114
114
|
| Maximum
|
|
115
115
|
| `{{maximum}}`
|
|
@@ -156,8 +156,15 @@ endif::[]
|
|
|
156
156
|
{{/if}}
|
|
157
157
|
|
|
158
158
|
ifndef::env-cloud[]
|
|
159
|
+
{{#if (ne gets_restored undefined)}}
|
|
160
|
+
|
|
161
|
+
| Restored on xref:manage:whole-cluster-restore.adoc[Whole Cluster Restore]
|
|
162
|
+
| {{#if gets_restored}}Yes{{else}}No{{/if}}
|
|
163
|
+
{{else if (eq rp_util_merge_status "unavailable")}}
|
|
164
|
+
|
|
159
165
|
| Restored on xref:manage:whole-cluster-restore.adoc[Whole Cluster Restore]
|
|
160
|
-
|
|
|
166
|
+
| Unknown (rp_util merge unavailable this build)
|
|
167
|
+
{{/if}}
|
|
161
168
|
endif::[]
|
|
162
169
|
{{#if visibility}}
|
|
163
170
|
|
|
@@ -101,17 +101,17 @@ ifndef::env-cloud[]
|
|
|
101
101
|
xref:reference:properties/{{#if cluster_property_doc_file}}{{cluster_property_doc_file}}{{else}}cluster-properties.adoc{{/if}}#{{corresponding_cluster_property}}[`{{corresponding_cluster_property}}`]
|
|
102
102
|
endif::[]
|
|
103
103
|
{{/if}}
|
|
104
|
-
{{#if (and minimum maximum)}}
|
|
104
|
+
{{#if (and (ne minimum undefined) (ne maximum undefined))}}
|
|
105
105
|
|
|
106
106
|
| Range
|
|
107
107
|
| [`{{minimum}}`, `{{maximum}}`]
|
|
108
108
|
{{else}}
|
|
109
|
-
{{#if minimum}}
|
|
109
|
+
{{#if (ne minimum undefined)}}
|
|
110
110
|
|
|
111
111
|
| Minimum
|
|
112
112
|
| `{{minimum}}`
|
|
113
113
|
{{/if}}
|
|
114
|
-
{{#if maximum}}
|
|
114
|
+
{{#if (ne maximum undefined)}}
|
|
115
115
|
|
|
116
116
|
| Maximum
|
|
117
117
|
| `{{maximum}}`
|
|
@@ -133,8 +133,11 @@ endif::[]
|
|
|
133
133
|
| {{#if nullable}}Yes{{else}}No{{/if}}
|
|
134
134
|
|
|
135
135
|
ifndef::env-cloud[]
|
|
136
|
+
{{#if (ne gets_restored undefined)}}
|
|
137
|
+
|
|
136
138
|
| Restored on xref:manage:whole-cluster-restore.adoc[Whole Cluster Restore]
|
|
137
|
-
| {{#if
|
|
139
|
+
| {{#if gets_restored}}Yes{{else}}No{{/if}}
|
|
140
|
+
{{/if}}
|
|
138
141
|
endif::[]
|
|
139
142
|
{{#if example}}
|
|
140
143
|
|