1pub const METADATA_VERSION_FEATURE: &str = "metadata.version";
14
15pub const METADATA_DOWNGRADE_CAPABILITY_FEATURE: &str = "krabka.metadata.downgrade";
22pub const METADATA_DOWNGRADE_CAPABILITY_LEVEL: i16 = 1;
24
25pub const SHARE_VERSION_FEATURE: &str = "share.version";
27pub const KRAFT_VERSION_FEATURE: &str = "kraft.version";
29pub const SHARE_VERSION_MIN: i16 = 0;
31pub const SHARE_VERSION_MAX: i16 = 2;
37
38pub const ELR_VERSION_FEATURE: &str = "eligible.leader.replicas.version";
41pub const ELR_VERSION_MIN: i16 = 0;
44pub const ELR_VERSION_MAX: i16 = 1;
47
48pub const STREAMS_VERSION_FEATURE: &str = "streams.version";
51pub const STREAMS_VERSION_MIN: i16 = 0;
53pub const STREAMS_VERSION_MAX: i16 = 1;
55
56pub const METADATA_VERSION_MIN: i16 = 7;
59pub const METADATA_VERSION_MAX: i16 = 34;
62
63pub const SCRAM_MIN_LEVEL: i16 = 11;
65pub const DELEGATION_TOKEN_MIN_LEVEL: i16 = 14;
67pub const ONLINE_DOWNGRADE_MIN_LEVEL: i16 = 15;
72pub const DIRECTORY_ASSIGNMENT_MIN_LEVEL: i16 = 17;
75pub const ELR_MIN_LEVEL: i16 = 23;
81pub const ELR_DEFAULT_METADATA_LEVEL: i16 = 26;
84pub const SHARE_VERSION_DEFAULT_METADATA_LEVEL: i16 = 28;
87pub const SHARE_VERSION_2_DEFAULT_METADATA_LEVEL: i16 = 31;
90pub const STREAMS_VERSION_DEFAULT_METADATA_LEVEL: i16 = 29;
93pub const CORDONED_LOG_DIRS_MIN_LEVEL: i16 = 30;
96pub const CIDR_ACL_MIN_LEVEL: i16 = 32;
98pub const CONTROLLER_UNREGISTRATION_MIN_LEVEL: i16 = 33;
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub struct MetadataVersion {
106 level: i16,
107 ivn: &'static str,
108 short: &'static str,
109 did_metadata_change: bool,
110}
111
112impl MetadataVersion {
113 #[must_use]
114 pub fn feature_level(self) -> i16 {
115 self.level
116 }
117 #[must_use]
118 pub fn ivn(self) -> &'static str {
119 self.ivn
120 }
121 #[must_use]
122 pub fn short(self) -> &'static str {
123 self.short
124 }
125 #[must_use]
128 pub fn did_metadata_change(self) -> bool {
129 self.did_metadata_change
130 }
131}
132
133const TABLE: &[MetadataVersion] = &[
134 MetadataVersion {
135 level: 7,
136 ivn: "3.3-IV3",
137 short: "3.3",
138 did_metadata_change: true,
139 },
140 MetadataVersion {
141 level: 8,
142 ivn: "3.4-IV0",
143 short: "3.4",
144 did_metadata_change: true,
145 },
146 MetadataVersion {
147 level: 9,
148 ivn: "3.5-IV0",
149 short: "3.5",
150 did_metadata_change: false,
151 },
152 MetadataVersion {
153 level: 10,
154 ivn: "3.5-IV1",
155 short: "3.5",
156 did_metadata_change: false,
157 },
158 MetadataVersion {
159 level: 11,
160 ivn: "3.5-IV2",
161 short: "3.5",
162 did_metadata_change: true,
163 },
164 MetadataVersion {
165 level: 12,
166 ivn: "3.6-IV0",
167 short: "3.6",
168 did_metadata_change: false,
169 },
170 MetadataVersion {
171 level: 13,
172 ivn: "3.6-IV1",
173 short: "3.6",
174 did_metadata_change: true,
175 },
176 MetadataVersion {
177 level: 14,
178 ivn: "3.6-IV2",
179 short: "3.6",
180 did_metadata_change: true,
181 },
182 MetadataVersion {
183 level: 15,
184 ivn: "3.7-IV0",
185 short: "3.7",
186 did_metadata_change: true,
187 },
188 MetadataVersion {
189 level: 16,
190 ivn: "3.7-IV1",
191 short: "3.7",
192 did_metadata_change: false,
193 },
194 MetadataVersion {
195 level: 17,
196 ivn: "3.7-IV2",
197 short: "3.7",
198 did_metadata_change: true,
199 },
200 MetadataVersion {
201 level: 18,
202 ivn: "3.7-IV3",
203 short: "3.7",
204 did_metadata_change: false,
205 },
206 MetadataVersion {
207 level: 19,
208 ivn: "3.7-IV4",
209 short: "3.7",
210 did_metadata_change: false,
211 },
212 MetadataVersion {
213 level: 20,
214 ivn: "3.8-IV0",
215 short: "3.8",
216 did_metadata_change: false,
217 },
218 MetadataVersion {
219 level: 21,
220 ivn: "3.9-IV0",
221 short: "3.9",
222 did_metadata_change: false,
223 },
224 MetadataVersion {
225 level: 22,
226 ivn: "4.0-IV0",
227 short: "4.0",
228 did_metadata_change: false,
229 },
230 MetadataVersion {
231 level: 23,
232 ivn: "4.0-IV1",
233 short: "4.0",
234 did_metadata_change: true,
235 },
236 MetadataVersion {
237 level: 24,
238 ivn: "4.0-IV2",
239 short: "4.0",
240 did_metadata_change: false,
241 },
242 MetadataVersion {
243 level: 25,
244 ivn: "4.0-IV3",
245 short: "4.0",
246 did_metadata_change: false,
247 },
248 MetadataVersion {
249 level: 26,
250 ivn: "4.1-IV0",
251 short: "4.1",
252 did_metadata_change: false,
253 },
254 MetadataVersion {
255 level: 27,
256 ivn: "4.1-IV1",
257 short: "4.1",
258 did_metadata_change: false,
259 },
260 MetadataVersion {
261 level: 28,
262 ivn: "4.2-IV0",
263 short: "4.2",
264 did_metadata_change: false,
265 },
266 MetadataVersion {
267 level: 29,
268 ivn: "4.2-IV1",
269 short: "4.2",
270 did_metadata_change: false,
271 },
272 MetadataVersion {
273 level: 30,
274 ivn: "4.3-IV0",
275 short: "4.3",
276 did_metadata_change: true,
277 },
278 MetadataVersion {
279 level: 31,
280 ivn: "4.4-IV0",
281 short: "4.4",
282 did_metadata_change: false,
283 },
284 MetadataVersion {
285 level: 32,
286 ivn: "4.4-IV1",
287 short: "4.4",
288 did_metadata_change: true,
289 },
290 MetadataVersion {
291 level: 33,
292 ivn: "4.4-IV2",
293 short: "4.4",
294 did_metadata_change: true,
295 },
296 MetadataVersion {
297 level: 34,
298 ivn: "4.5-IV0",
299 short: "4.5",
300 did_metadata_change: false,
301 },
302];
303
304#[must_use]
307pub fn from_feature_level(level: i16) -> Option<MetadataVersion> {
308 TABLE.iter().copied().find(|m| m.level == level)
309}
310
311#[must_use]
317pub fn from_version_string(s: &str) -> Option<MetadataVersion> {
318 let s = s.trim();
319 if s.contains('-') {
320 return TABLE.iter().copied().find(|m| m.ivn == s);
321 }
322 TABLE
323 .iter()
324 .copied()
325 .filter(|m| m.short == s)
326 .max_by_key(|m| m.level)
327}
328
329#[must_use]
331pub fn is_supported_level(level: i16) -> bool {
332 (METADATA_VERSION_MIN..=METADATA_VERSION_MAX).contains(&level)
333}
334
335#[must_use]
341pub fn did_metadata_change(level: i16) -> Option<bool> {
342 from_feature_level(level).map(MetadataVersion::did_metadata_change)
343}
344
345#[must_use]
356pub fn metadata_changed_between(from: i16, to: i16) -> bool {
357 let (low, high) = if from <= to { (from, to) } else { (to, from) };
358 ((low + 1)..=high).any(|level| did_metadata_change(level).unwrap_or(true))
359}
360
361#[cfg(test)]
362mod tests {
363 use assert2::check;
364
365 use super::*;
366
367 const KAFKA_DID_METADATA_CHANGE: [(i16, bool); 28] = [
370 (7, true),
371 (8, true),
372 (9, false),
373 (10, false),
374 (11, true),
375 (12, false),
376 (13, true),
377 (14, true),
378 (15, true),
379 (16, false),
380 (17, true),
381 (18, false),
382 (19, false),
383 (20, false),
384 (21, false),
385 (22, false),
386 (23, true),
387 (24, false),
388 (25, false),
389 (26, false),
390 (27, false),
391 (28, false),
392 (29, false),
393 (30, true),
394 (31, false),
395 (32, true),
396 (33, true),
397 (34, false),
398 ];
399
400 #[test]
401 fn did_metadata_change_matches_kafka() {
402 let table: Vec<(i16, bool)> = (METADATA_VERSION_MIN..=METADATA_VERSION_MAX)
403 .map(|level| (level, did_metadata_change(level).expect("supported level")))
404 .collect();
405 check!(table == KAFKA_DID_METADATA_CHANGE);
406 check!(
407 (did_metadata_change(6), did_metadata_change(35)) == (None, None),
408 "levels outside the table"
409 );
410 }
411
412 #[test]
417 fn metadata_changed_between_matches_kafka() {
418 for (case, from, to, want) in [
419 ("same level", 25, 25, false),
420 ("same changing level", 30, 30, false),
421 ("4.0-IV3 to 4.2-IV1 changes nothing", 25, 29, false),
422 ("4.2-IV1 down to 4.0-IV3 changes nothing", 29, 25, false),
423 ("up into 4.3-IV0 cordoned log dirs", 29, 30, true),
424 ("down out of 4.3-IV0", 30, 29, true),
425 ("4.3-IV0 to 4.4-IV0 changes nothing", 30, 31, false),
426 ("4.4-IV0 down to 4.3-IV0 changes nothing", 31, 30, false),
427 ("down out of 4.4-IV1 CIDR ACLs", 32, 31, true),
428 ("down across ELR records at 4.0-IV1", 25, 22, true),
429 ("down to 4.0-IV1 itself", 25, 23, false),
430 ("3.5-IV0 to 3.5-IV1", 9, 10, false),
431 ("3.5-IV1 up to SCRAM at 3.5-IV2", 10, 11, true),
432 ("3.7-IV2 to 3.7-IV4", 17, 19, false),
433 ("3.7-IV0 to 3.7-IV1", 15, 16, false),
434 ("3.7-IV1 up to JBOD at 3.7-IV2", 16, 17, true),
435 ("minimum to 3.4-IV0", 7, 8, true),
436 (
437 "down out of 4.4-IV2 controller unregistration",
438 33,
439 32,
440 true,
441 ),
442 ("4.4-IV2 to 4.5-IV0 changes nothing", 33, 34, false),
443 ("4.5-IV0 down to 4.4-IV2 changes nothing", 34, 33, false),
444 ("4.5-IV0 down through 4.4-IV2", 34, 32, true),
445 ("whole table", 7, 34, true),
446 ("below the table", 6, 7, true),
447 ("above the table", 34, 35, true),
448 ] {
449 check!(metadata_changed_between(from, to) == want, "{case}");
450 }
451 }
452
453 #[test]
454 fn min_max_levels() {
455 check!(
456 (
457 METADATA_VERSION_MIN,
458 METADATA_VERSION_MAX,
459 TABLE.first().unwrap().level,
460 TABLE.last().unwrap().level,
461 ) == (7, 34, METADATA_VERSION_MIN, METADATA_VERSION_MAX)
462 );
463 }
464
465 #[test]
468 fn table_is_contiguous_and_ordered() {
469 let levels: Vec<i16> = TABLE.iter().map(|m| m.level).collect();
470 let want: Vec<i16> = (METADATA_VERSION_MIN..=METADATA_VERSION_MAX).collect();
471 check!(levels == want);
472 }
473
474 #[test]
475 fn share_version_feature_levels() {
476 check!(
477 (SHARE_VERSION_FEATURE, SHARE_VERSION_MIN, SHARE_VERSION_MAX)
478 == ("share.version", 0, 2)
479 );
480 }
481
482 #[test]
483 fn elr_version_feature_levels() {
484 check!(
485 (ELR_VERSION_FEATURE, ELR_VERSION_MIN, ELR_VERSION_MAX)
486 == ("eligible.leader.replicas.version", 0, 1)
487 );
488 }
489
490 #[test]
491 fn streams_version_feature_levels() {
492 check!(
493 (
494 STREAMS_VERSION_FEATURE,
495 STREAMS_VERSION_MIN,
496 STREAMS_VERSION_MAX
497 ) == ("streams.version", 0, 1)
498 );
499 }
500
501 #[test]
502 fn from_feature_level_known_and_unknown() {
503 for (level, want) in [
504 (
505 7,
506 Some(MetadataVersion {
507 level: 7,
508 ivn: "3.3-IV3",
509 short: "3.3",
510 did_metadata_change: true,
511 }),
512 ),
513 (
514 25,
515 Some(MetadataVersion {
516 level: 25,
517 ivn: "4.0-IV3",
518 short: "4.0",
519 did_metadata_change: false,
520 }),
521 ),
522 (
523 30,
524 Some(MetadataVersion {
525 level: 30,
526 ivn: "4.3-IV0",
527 short: "4.3",
528 did_metadata_change: true,
529 }),
530 ),
531 (
532 32,
533 Some(MetadataVersion {
534 level: 32,
535 ivn: "4.4-IV1",
536 short: "4.4",
537 did_metadata_change: true,
538 }),
539 ),
540 (
541 33,
542 Some(MetadataVersion {
543 level: 33,
544 ivn: "4.4-IV2",
545 short: "4.4",
546 did_metadata_change: true,
547 }),
548 ),
549 (
550 34,
551 Some(MetadataVersion {
552 level: 34,
553 ivn: "4.5-IV0",
554 short: "4.5",
555 did_metadata_change: false,
556 }),
557 ),
558 (6, None),
559 (35, None),
560 ] {
561 assert2::assert!(from_feature_level(level) == want);
562 }
563 }
564
565 #[test]
569 fn accessors_return_the_table_entry() {
570 let v = from_feature_level(25).expect("level 25 is in the table");
571 assert2::check!((v.feature_level(), v.ivn(), v.short()) == (25, "4.0-IV3", "4.0"));
572
573 let earliest = from_feature_level(7).expect("level 7 is in the table");
574 assert2::check!((earliest.ivn(), earliest.short()) == ("3.3-IV3", "3.3"));
575 }
576
577 #[test]
578 fn from_version_string_exact_ivn() {
579 for (_case, s, want) in [
580 ("known 3.5 IV", "3.5-IV2", Some(11)),
581 ("known 4.0 IV", "4.0-IV3", Some(25)),
582 ("known 4.3 IV", "4.3-IV0", Some(30)),
583 ("known 4.4 IV", "4.4-IV1", Some(32)),
584 ("last 4.4 IV", "4.4-IV2", Some(33)),
585 ("reserved 4.4 IV", "4.4-IV3", None),
586 ("latest 4.5 IV", "4.5-IV0", Some(34)),
587 ("reserved 4.5 IV", "4.5-IV1", None),
588 ("unknown IV", "3.5-IV9", None),
589 ] {
590 assert2::assert!(
591 from_version_string(s).map(super::MetadataVersion::feature_level) == want
592 );
593 }
594 }
595
596 #[test]
597 fn from_version_string_short_picks_highest_in_minor() {
598 for (_case, s, want) in [
599 ("known 3.7 minor", "3.7", Some(19)),
600 ("known 4.0 minor", "4.0", Some(25)),
601 ("known 4.1 minor", "4.1", Some(27)),
602 ("known 4.2 minor", "4.2", Some(29)),
603 ("known 4.3 minor", "4.3", Some(30)),
604 ("known 4.4 minor", "4.4", Some(33)),
605 ("known 4.5 minor", "4.5", Some(34)),
606 ("unsupported minor", "2.8", None),
607 ("unreleased minor", "4.6", None),
608 ] {
609 assert2::assert!(
610 from_version_string(s).map(super::MetadataVersion::feature_level) == want
611 );
612 }
613 }
614
615 #[test]
616 fn in_supported_range_predicate() {
617 for (_case, level, want) in [
618 ("minimum", 7, true),
619 ("maximum", 34, true),
620 ("below minimum", 6, false),
621 ("above maximum", 35, false),
622 ] {
623 assert2::assert!(is_supported_level(level) == want);
624 }
625 }
626
627 #[test]
628 fn gate_level_constants() {
629 for (case, level, expected_ivn) in [
630 ("SCRAM gate", SCRAM_MIN_LEVEL, "3.5-IV2"),
631 (
632 "delegation-token gate",
633 DELEGATION_TOKEN_MIN_LEVEL,
634 "3.6-IV2",
635 ),
636 (
637 "controller registration gate",
638 ONLINE_DOWNGRADE_MIN_LEVEL,
639 "3.7-IV0",
640 ),
641 (
642 "directory assignment gate",
643 DIRECTORY_ASSIGNMENT_MIN_LEVEL,
644 "3.7-IV2",
645 ),
646 ("ELR record gate", ELR_MIN_LEVEL, "4.0-IV1"),
647 ("ELRV_1 bootstrap", ELR_DEFAULT_METADATA_LEVEL, "4.1-IV0"),
648 (
649 "SV_1 bootstrap",
650 SHARE_VERSION_DEFAULT_METADATA_LEVEL,
651 "4.2-IV0",
652 ),
653 (
654 "share SV_2 bootstrap",
655 SHARE_VERSION_2_DEFAULT_METADATA_LEVEL,
656 "4.4-IV0",
657 ),
658 (
659 "streams SV_1 bootstrap",
660 STREAMS_VERSION_DEFAULT_METADATA_LEVEL,
661 "4.2-IV1",
662 ),
663 (
664 "cordoned log dirs gate",
665 CORDONED_LOG_DIRS_MIN_LEVEL,
666 "4.3-IV0",
667 ),
668 ("CIDR ACL gate", CIDR_ACL_MIN_LEVEL, "4.4-IV1"),
669 (
670 "controller unregistration gate",
671 CONTROLLER_UNREGISTRATION_MIN_LEVEL,
672 "4.4-IV2",
673 ),
674 ] {
675 check!(
676 from_feature_level(level).unwrap().ivn() == expected_ivn,
677 "case {case}"
678 );
679 }
680 }
681}