Skip to main content

krabka_metadata/
metadata_version.rs

1//! KIP-778 `metadata.version` feature-level model.
2//!
3//! The canonical string<->integer-level table, mirrored byte-for-byte from
4//! upstream Kafka's `MetadataVersion` enum over the range Krabka advertises
5//! (`[METADATA_VERSION_MIN, METADATA_VERSION_MAX]`). JVM clients call
6//! `MetadataVersion.fromFeatureLevel(N)` and throw on any level their enum does
7//! not know, so the levels and `X.Y-IVn` names here MUST match upstream
8//! exactly. Verify against `MetadataVersion.java` in Kafka's `server-common`
9//! module on trunk before editing: levels are never reused, so the table must
10//! carry every level up to [`METADATA_VERSION_MAX`], reserved ones included.
11
12/// The `metadata.version` feature name (KIP-584 / KIP-778).
13pub const METADATA_VERSION_FEATURE: &str = "metadata.version";
14
15/// Krabka registration-only marker for KIP-1155 downgrade support.
16///
17/// The KIP is still under discussion and has not assigned its promised
18/// capability `metadata.version` level, so this must not extend the canonical
19/// metadata-version range or appear in `ApiVersions`. It is carried only in
20/// broker/controller registration feature maps; pre-KIP JVM nodes omit it.
21pub const METADATA_DOWNGRADE_CAPABILITY_FEATURE: &str = "krabka.metadata.downgrade";
22/// The only supported level of [`METADATA_DOWNGRADE_CAPABILITY_FEATURE`].
23pub const METADATA_DOWNGRADE_CAPABILITY_LEVEL: i16 = 1;
24
25/// The `share.version` feature name (KIP-932). Gates share-group membership.
26pub const SHARE_VERSION_FEATURE: &str = "share.version";
27/// KIP-853 Raft protocol and dynamic-membership feature.
28pub const KRAFT_VERSION_FEATURE: &str = "kraft.version";
29/// Minimum supported `share.version` level: `0` (feature disabled).
30pub const SHARE_VERSION_MIN: i16 = 0;
31/// Maximum supported `share.version` level: `2`, trunk's `ShareVersion.SV_2`
32/// (KIP-1191 dead-letter queues).
33///
34/// Kafka 4.3.1 stops at `1` (KIP-932 GA); a node caps itself there unless it
35/// supports unstable feature versions.
36pub const SHARE_VERSION_MAX: i16 = 2;
37
38/// The `eligible.leader.replicas.version` feature name (KIP-966). Gates the
39/// controller's maintenance of eligible leader replicas.
40pub const ELR_VERSION_FEATURE: &str = "eligible.leader.replicas.version";
41/// Minimum supported `eligible.leader.replicas.version` level: `0`, `ELRV_0`,
42/// the level at which the controller keeps no ELR.
43pub const ELR_VERSION_MIN: i16 = 0;
44/// Maximum supported `eligible.leader.replicas.version` level: `1`, `ELRV_1`,
45/// which turns KIP-966 ELR maintenance on.
46pub const ELR_VERSION_MAX: i16 = 1;
47
48/// The `streams.version` feature name (KIP-1071). Gates the broker-side
49/// Streams rebalance protocol (`StreamsGroupHeartbeat` / `StreamsGroupDescribe`).
50pub const STREAMS_VERSION_FEATURE: &str = "streams.version";
51/// Minimum supported `streams.version` level: `0` (feature disabled).
52pub const STREAMS_VERSION_MIN: i16 = 0;
53/// Maximum supported `streams.version` level: `1` (KIP-1071 early access).
54pub const STREAMS_VERSION_MAX: i16 = 1;
55
56/// Minimum supported level: `3.3-IV3` (`KRaft` GA), the floor that real Kafka
57/// 4.0 supports.
58pub const METADATA_VERSION_MIN: i16 = 7;
59/// Maximum supported level: `4.5-IV0`, Kafka trunk's `latestTesting` (the
60/// Kafka 4.5.0 release level, which changes no metadata record format).
61pub const METADATA_VERSION_MAX: i16 = 34;
62
63/// Level at which `KRaft` gained SCRAM credentials (`3.5-IV2`).
64pub const SCRAM_MIN_LEVEL: i16 = 11;
65/// Level at which `KRaft` gained delegation tokens (`3.6-IV2`).
66pub const DELEGATION_TOKEN_MIN_LEVEL: i16 = 14;
67/// Lowest level that supports controller registrations (`3.7-IV0`).
68///
69/// Online downgrades cannot cross this boundary because the active controller
70/// needs those registrations to verify every quorum member supports the target.
71pub const ONLINE_DOWNGRADE_MIN_LEVEL: i16 = 15;
72/// Level at which partition directory assignments became part of the `KRaft`
73/// metadata records (`3.7-IV2`, KIP-858).
74pub const DIRECTORY_ASSIGNMENT_MIN_LEVEL: i16 = 17;
75/// Level at which partition records gained KIP-966 eligible-leader fields
76/// (`4.0-IV1`).
77///
78/// It selects the record version Kafka readers expect, and it is what
79/// [`crate::feature::ElrVersionFeature`] depends on at level 1.
80pub const ELR_MIN_LEVEL: i16 = 23;
81/// Level at which `ELRV_1` becomes the bootstrap default (`4.1-IV0`,
82/// `EligibleLeaderReplicasVersion.ELRV_1`'s bootstrap metadata version).
83pub const ELR_DEFAULT_METADATA_LEVEL: i16 = 26;
84/// Level at which `share.version` 1 becomes the bootstrap default (`4.2-IV0`,
85/// `ShareVersion.SV_1`'s bootstrap metadata version, KIP-932 GA).
86pub const SHARE_VERSION_DEFAULT_METADATA_LEVEL: i16 = 28;
87/// Level at which `share.version` 2 becomes the bootstrap default (`4.4-IV0`,
88/// trunk's `ShareVersion.SV_2` bootstrap metadata version, KIP-1191).
89pub const SHARE_VERSION_2_DEFAULT_METADATA_LEVEL: i16 = 31;
90/// Level at which `streams.version` 1 becomes the bootstrap default (`4.2-IV1`,
91/// `StreamsVersion.SV_1`'s bootstrap metadata version, KIP-1071 GA).
92pub const STREAMS_VERSION_DEFAULT_METADATA_LEVEL: i16 = 29;
93/// Level at which `RegisterBrokerRecord` and `BrokerRegistrationChangeRecord`
94/// carry KIP-1066 cordoned log directories (`4.3-IV0`).
95pub const CORDONED_LOG_DIRS_MIN_LEVEL: i16 = 30;
96/// Level at which ACL host patterns may be CIDR blocks (`4.4-IV1`, KIP-1276).
97pub const CIDR_ACL_MIN_LEVEL: i16 = 32;
98/// Level at which a controller may be unregistered (`4.4-IV2`, KIP-1312):
99/// Kafka's `MetadataVersion.isControllerUnregistrationSupported`.
100pub const CONTROLLER_UNREGISTRATION_MIN_LEVEL: i16 = 33;
101
102/// One `metadata.version` level: its integer feature level, canonical
103/// `X.Y-IVn` name, short `X.Y` form, and Kafka's `didMetadataChange` flag.
104#[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    /// Kafka's `MetadataVersion.didMetadataChange`: whether this level changed
126    /// the metadata record format, so a downgrade across it can lose data.
127    #[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/// Look up a level by integer feature level. `None` if outside the
305/// supported table.
306#[must_use]
307pub fn from_feature_level(level: i16) -> Option<MetadataVersion> {
308    TABLE.iter().copied().find(|m| m.level == level)
309}
310
311/// Resolve a version string to a level.
312///
313/// The function accepts both the exact `X.Y-IVn` form and the short `X.Y` form.
314/// The short form resolves to the highest level within that minor, which
315/// matches `MetadataVersion.fromVersionString`.
316#[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/// True if `level` is within `[METADATA_VERSION_MIN, METADATA_VERSION_MAX]`.
330#[must_use]
331pub fn is_supported_level(level: i16) -> bool {
332    (METADATA_VERSION_MIN..=METADATA_VERSION_MAX).contains(&level)
333}
334
335/// Kafka's `didMetadataChange` flag for `level`, or `None` for a level outside
336/// the supported table.
337///
338/// Levels up to `4.3-IV0` (30) follow Kafka 4.3.1, and `4.4-IV0` (31) to
339/// `4.5-IV0` (34) follow Kafka trunk.
340#[must_use]
341pub fn did_metadata_change(level: i16) -> Option<bool> {
342    from_feature_level(level).map(MetadataVersion::did_metadata_change)
343}
344
345/// Kafka's `MetadataVersion.checkIfMetadataChanged`: whether moving between
346/// `from` and `to`, in either direction, crosses a level that changed the
347/// metadata record format. Equal levels never do.
348///
349/// Kafka walks down from the higher level and answers false only when it
350/// reaches the lower level without passing a changing level. The lower level
351/// itself does not count, because a cluster already at it has its records. A
352/// level outside the supported table counts as changing, so a range that
353/// leaves the table answers true, as Kafka does when it runs out of
354/// predecessors.
355#[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    /// Kafka's `didMetadataChange` flag per level: `MetadataVersion.java` at
368    /// 4.3.1 for 7-30, at trunk for 31 (`4.4-IV0`) to 34 (`4.5-IV0`).
369    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    /// Cases worked through Kafka's `checkIfMetadataChangedOrdered` by hand:
413    /// walk down from the higher level while the current level did not change
414    /// metadata and is not the lower level; the answer is whether the walk
415    /// stopped before reaching the lower level.
416    #[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    /// Kafka never reuses or skips a level, so the table must be every level
466    /// from the minimum to the maximum, in order.
467    #[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    /// The table is compared as whole structs elsewhere, which checks the
566    /// fields but never calls the accessors that read them back. `short()`
567    /// answering a constant is what the release-version parsing keys off.
568    #[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}