1pub use krabka_ids::LeaderEpoch;
11pub use krabka_voters::NodeId;
12use serde::{Deserialize, Serialize};
13use uuid::Uuid;
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct TopicRecord {
17 pub name: String,
18 pub topic_id: Uuid,
19 pub partitions: i32,
20 pub replication_factor: i16,
21}
22
23fn default_partition_epoch() -> i32 {
24 -1
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
28pub struct PartitionRecord {
29 pub topic: String,
30 pub partition: i32,
31 pub leader: NodeId,
32 pub replicas: Vec<NodeId>,
33 pub isr: Vec<NodeId>,
34 pub leader_epoch: LeaderEpoch,
39 pub adding_replicas: Vec<NodeId>,
42 pub removing_replicas: Vec<NodeId>,
45 pub directories: Vec<Uuid>,
58 #[serde(default = "default_partition_epoch")]
64 pub partition_epoch: i32,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75pub struct PartitionDirAssignmentRecord {
76 pub topic: String,
77 pub partition: i32,
78 pub replica: NodeId,
80 pub directory: Uuid,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91pub struct PartitionOffsetAdvanceRecord {
92 pub topic: String,
93 pub partition: i32,
94 pub count: i64,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101pub struct PartitionElrRecord {
102 pub topic: String,
103 pub partition: i32,
104 pub eligible_leader_replicas: Vec<NodeId>,
105 pub last_known_elr: Vec<NodeId>,
106}
107
108#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
110#[repr(i8)]
111pub enum LeaderRecoveryState {
112 #[default]
113 Recovered = 0,
114 Recovering = 1,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
118pub struct PartitionRecoveryRecord {
119 pub topic: String,
120 pub partition: i32,
121 pub state: LeaderRecoveryState,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127pub struct PartitionUpdateRecord {
128 pub partition: PartitionRecord,
129 pub eligible_leader_replicas: Option<Vec<NodeId>>,
130 pub last_known_elr: Option<Vec<NodeId>>,
131 pub recovery_state: Option<LeaderRecoveryState>,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140pub struct BrokerEndpoint {
141 pub name: String,
143 pub host: String,
144 pub port: u16,
145 pub protocol: krabka_security::ListenerProtocol,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149pub struct BrokerRegistrationRecord {
150 pub node_id: NodeId,
151 pub broker_epoch: i64,
157 #[serde(default)]
162 pub incarnation_id: uuid::Uuid,
163 pub host: String,
166 pub port: u16,
167 pub rack: Option<String>,
168 pub endpoints: Vec<BrokerEndpoint>,
172 #[serde(default)]
175 pub log_dirs: Vec<uuid::Uuid>,
176 pub fenced: bool,
180 pub in_controlled_shutdown: bool,
184 pub cordoned_log_dirs: Option<Vec<uuid::Uuid>>,
188 #[serde(default)]
192 pub features: std::collections::BTreeMap<String, (i16, i16)>,
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
202pub struct ControllerRegistrationRecord {
203 pub node_id: NodeId,
204 pub incarnation_id: Uuid,
205 pub zk_migration_ready: bool,
206 pub endpoints: Vec<BrokerEndpoint>,
207 pub features: std::collections::BTreeMap<String, (i16, i16)>,
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
211pub struct DeleteTopicRecord {
212 pub name: String,
213}
214
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
227pub struct UnregisterBrokerRecord {
228 pub node_id: NodeId,
229 pub broker_epoch: i64,
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
237pub enum FencingChange {
238 Unfence,
240 None,
242 Fence,
244}
245
246impl FencingChange {
247 #[must_use]
249 pub fn wire_value(self) -> i8 {
250 match self {
251 Self::Unfence => -1,
252 Self::None => 0,
253 Self::Fence => 1,
254 }
255 }
256
257 #[must_use]
259 pub fn from_wire(value: i8) -> Option<Self> {
260 match value {
261 -1 => Some(Self::Unfence),
262 0 => Some(Self::None),
263 1 => Some(Self::Fence),
264 _ => None,
265 }
266 }
267
268 #[must_use]
270 pub fn apply(self, fenced: bool) -> bool {
271 match self {
272 Self::Unfence => false,
273 Self::None => fenced,
274 Self::Fence => true,
275 }
276 }
277}
278
279#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
290pub struct BrokerRegistrationChangeRecord {
291 pub node_id: NodeId,
292 pub broker_epoch: i64,
293 pub fenced: FencingChange,
294 pub in_controlled_shutdown: bool,
295 pub log_dirs: Vec<Uuid>,
297 pub cordoned_log_dirs: Option<Vec<Uuid>>,
299}
300
301impl BrokerRegistrationChangeRecord {
302 #[must_use]
305 pub fn no_change(node_id: NodeId, broker_epoch: i64) -> Self {
306 Self {
307 node_id,
308 broker_epoch,
309 fenced: FencingChange::None,
310 in_controlled_shutdown: false,
311 log_dirs: Vec::new(),
312 cordoned_log_dirs: None,
313 }
314 }
315
316 #[must_use]
319 pub fn applied_to(&self, current: &BrokerRegistrationRecord) -> BrokerRegistrationRecord {
320 BrokerRegistrationRecord {
321 fenced: self.fenced.apply(current.fenced),
322 in_controlled_shutdown: current.in_controlled_shutdown || self.in_controlled_shutdown,
323 log_dirs: if self.log_dirs.is_empty() {
324 current.log_dirs.clone()
325 } else {
326 self.log_dirs.clone()
327 },
328 cordoned_log_dirs: self
329 .cordoned_log_dirs
330 .clone()
331 .or_else(|| current.cordoned_log_dirs.clone()),
332 ..current.clone()
333 }
334 }
335}
336
337#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
342pub struct UnregisterControllerRecord {
343 pub node_id: NodeId,
344}
345
346#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
351pub struct TopicConfigRecord {
352 pub topic: String,
353 pub overrides: std::collections::BTreeMap<String, String>,
354}
355
356#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
362pub struct BrokerConfigRecord {
363 pub node_id: NodeId,
364 pub config_name: String,
365 pub config_value: Option<String>,
367}
368
369pub const DEFAULT_BROKER_CONFIG_NODE_ID: NodeId = NodeId(u64::MAX);
373
374#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
380pub struct ClientMetricsConfigRecord {
381 pub name: String,
382 pub configs: std::collections::BTreeMap<String, String>,
383}
384
385#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
388pub struct GroupConfigRecord {
389 pub group_id: String,
390 pub configs: std::collections::BTreeMap<String, String>,
391}
392
393#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
394pub struct QuotaEntity {
395 pub entity_type: String,
396 pub entity_name: Option<String>,
397}
398
399#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
400pub struct ClientQuotaRecord {
401 pub entity: Vec<QuotaEntity>,
403 pub config_key: String,
404 pub config_value: Option<f64>,
405}
406
407#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
410pub struct ProducerIdsRecord {
411 pub broker_id: NodeId,
412 pub broker_epoch: i64,
413 pub next_producer_id: i64,
414}
415
416#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
417pub struct ScramCredentialRecord {
418 pub user: String,
419 pub mechanism: krabka_security::SaslMechanism,
420 pub salt: Vec<u8>,
421 pub stored_key: Vec<u8>,
422 pub server_key: Vec<u8>,
423 pub iterations: u32,
424}
425
426#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
427pub struct DeleteScramCredentialRecord {
428 pub user: String,
429 pub mechanism: krabka_security::SaslMechanism,
430}
431
432#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
443pub struct DelegationTokenRecord {
444 pub token_id: String,
445 pub owner: krabka_security::KafkaPrincipal,
446 pub requester: krabka_security::KafkaPrincipal,
449 pub issue_timestamp_ms: i64,
450 pub expiry_timestamp_ms: i64,
451 pub max_timestamp_ms: i64,
454 pub renewers: Vec<krabka_security::KafkaPrincipal>,
455}
456
457#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
461pub struct DeleteDelegationTokenRecord {
462 pub token_id: String,
463}
464
465#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
467pub struct KRaftVersionRecord {
468 pub kraft_version: u16,
469}
470
471#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
473pub struct VotersRecord {
474 pub voters: crate::voters::VoterSet,
475}
476
477#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
483pub struct FeatureLevelRecord {
484 pub name: String,
485 pub level: i16,
486}
487
488#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
506pub struct FeaturesEpochRecord {
507 pub epoch: i64,
508}
509
510#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
511#[non_exhaustive]
512pub enum MetadataRecord {
513 V1Topic(TopicRecord),
514 V1Partition(PartitionRecord),
515 V1BrokerRegistration(BrokerRegistrationRecord),
516 V1DeleteTopic(DeleteTopicRecord),
517 V1TopicConfig(TopicConfigRecord),
518 V1ScramCredential(ScramCredentialRecord),
519 V1DeleteScramCredential(DeleteScramCredentialRecord),
520 V1AccessControlEntry(crate::AclEntry),
521 V1DeleteAccessControlEntry(crate::AclEntryFilter),
522 V1BrokerConfig(BrokerConfigRecord),
523 V1ClientQuota(ClientQuotaRecord),
524 V1ProducerIds(ProducerIdsRecord),
525 V1DelegationToken(DelegationTokenRecord),
526 V1DeleteDelegationToken(DeleteDelegationTokenRecord),
527 V1UnregisterBroker(UnregisterBrokerRecord),
528 V1KRaftVersion(KRaftVersionRecord),
529 V1Voters(VotersRecord),
530 V1FeatureLevel(FeatureLevelRecord),
531 V1ClientMetricsConfig(ClientMetricsConfigRecord),
532 V1FeaturesEpoch(FeaturesEpochRecord),
535 V1PartitionDirAssignment(PartitionDirAssignmentRecord),
539 V1PartitionOffsetAdvance(PartitionOffsetAdvanceRecord),
542 V1GroupConfig(GroupConfigRecord),
544 V1ControllerRegistration(ControllerRegistrationRecord),
546 V1TopicFreeze(crate::write_freeze::TopicFreezeRecord),
551 V1BreakGlassProposal(crate::break_glass::BreakGlassProposalRecord),
556 V1DeleteBreakGlassProposal(Uuid),
559 V1PartitionElr(PartitionElrRecord),
561 V1PartitionRecovery(PartitionRecoveryRecord),
563 V1PartitionUpdate(PartitionUpdateRecord),
565 V1BrokerRegistrationChange(BrokerRegistrationChangeRecord),
569 V1UnregisterController(UnregisterControllerRecord),
571}
572
573#[cfg(test)]
574mod tests {
575
576 use serde_wincode::SerdeCompat;
577 use wincode::{Deserialize as _, Serialize as _};
578
579 use super::*;
580
581 fn round_trip(r: &MetadataRecord) -> MetadataRecord {
582 let bytes = <SerdeCompat<MetadataRecord>>::serialize(r).unwrap();
583 <SerdeCompat<MetadataRecord>>::deserialize(&bytes).unwrap()
584 }
585
586 #[test]
587 fn feature_level_round_trip() {
588 let r = MetadataRecord::V1FeatureLevel(FeatureLevelRecord {
589 name: "metadata.version".into(),
590 level: 1,
591 });
592 assert2::assert!(round_trip(&r) == r);
593 }
594
595 #[test]
596 fn group_config_round_trip() {
597 let r = MetadataRecord::V1GroupConfig(GroupConfigRecord {
598 group_id: "streams-app".into(),
599 configs: std::collections::BTreeMap::from([(
600 "streams.num.standby.replicas".into(),
601 "1".into(),
602 )]),
603 });
604 assert2::assert!(round_trip(&r) == r);
605 }
606
607 #[test]
608 fn controller_registration_round_trip() {
609 let r = MetadataRecord::V1ControllerRegistration(ControllerRegistrationRecord {
610 node_id: NodeId(3),
611 incarnation_id: Uuid::from_u128(7),
612 zk_migration_ready: false,
613 endpoints: vec![BrokerEndpoint {
614 name: "CONTROLLER".into(),
615 host: "controller-3".into(),
616 port: 9093,
617 protocol: krabka_security::ListenerProtocol::Plaintext,
618 }],
619 features: std::collections::BTreeMap::from([("metadata.version".into(), (7, 25))]),
620 });
621 assert2::assert!(round_trip(&r) == r);
622 }
623
624 #[test]
625 fn producer_ids_round_trip() {
626 let r = MetadataRecord::V1ProducerIds(ProducerIdsRecord {
627 broker_id: NodeId(3),
628 broker_epoch: 9,
629 next_producer_id: 2_000,
630 });
631 assert2::assert!(round_trip(&r) == r);
632 }
633
634 #[test]
635 fn features_epoch_round_trip() {
636 let r = MetadataRecord::V1FeaturesEpoch(FeaturesEpochRecord { epoch: 7 });
637 assert2::assert!(round_trip(&r) == r);
638 }
639
640 #[test]
641 fn topic_record_round_trip() {
642 let r = MetadataRecord::V1Topic(TopicRecord {
643 name: "t".into(),
644 topic_id: Uuid::new_v4(),
645 partitions: 3,
646 replication_factor: 1,
647 });
648 assert2::assert!(round_trip(&r) == r);
649 }
650
651 #[test]
652 fn partition_record_round_trip() {
653 let r = MetadataRecord::V1Partition(PartitionRecord {
654 topic: "t".into(),
655 partition: 0,
656 leader: NodeId(1),
657 replicas: vec![NodeId(1), NodeId(2), NodeId(3)],
658 isr: vec![NodeId(1), NodeId(2)],
659 leader_epoch: LeaderEpoch(0),
660 adding_replicas: vec![],
661 removing_replicas: vec![],
662 directories: vec![Uuid::from_u128(1), Uuid::from_u128(2), Uuid::nil()],
663 partition_epoch: 0,
664 });
665 assert2::assert!(round_trip(&r) == r);
666 }
667
668 #[test]
669 fn partition_dir_assignment_round_trip() {
670 let r = MetadataRecord::V1PartitionDirAssignment(PartitionDirAssignmentRecord {
671 topic: "t".into(),
672 partition: 2,
673 replica: NodeId(3),
674 directory: Uuid::from_u128(0xAB),
675 });
676 assert2::assert!(round_trip(&r) == r);
677 }
678
679 #[test]
680 fn broker_registration_round_trip() {
681 let r = MetadataRecord::V1BrokerRegistration(BrokerRegistrationRecord {
682 node_id: NodeId(7),
683 broker_epoch: 0,
684 incarnation_id: Uuid::from_u128(0xdeadbeef_cafe_babe_0123_456789abcdef),
685 host: "192.168.1.10".into(),
686 port: 9092,
687 rack: Some("us-east-1a".into()),
688 log_dirs: vec![],
689 fenced: false,
690 in_controlled_shutdown: false,
691 cordoned_log_dirs: None,
692 endpoints: vec![],
693 features: std::collections::BTreeMap::new(),
694 });
695 assert2::assert!(round_trip(&r) == r);
696 }
697
698 #[test]
699 fn broker_registration_with_endpoints_round_trip() {
700 let r = MetadataRecord::V1BrokerRegistration(BrokerRegistrationRecord {
701 node_id: NodeId(1),
702 broker_epoch: 0,
703 incarnation_id: Uuid::from_u128(0xfeedface_0000_0000_0000_000000000001),
704 host: "h".into(),
705 port: 9092,
706 rack: None,
707 log_dirs: vec![],
708 fenced: false,
709 in_controlled_shutdown: false,
710 cordoned_log_dirs: None,
711 endpoints: vec![BrokerEndpoint {
712 name: "EXTERNAL".into(),
713 host: "ext.example.com".into(),
714 port: 9092,
715 protocol: krabka_security::ListenerProtocol::SaslSsl,
716 }],
717 features: std::collections::BTreeMap::new(),
718 });
719 assert2::assert!(round_trip(&r) == r);
720 }
721
722 #[test]
723 fn delete_topic_round_trip() {
724 let r = MetadataRecord::V1DeleteTopic(DeleteTopicRecord {
725 name: "doomed".into(),
726 });
727 assert2::assert!(round_trip(&r) == r);
728 }
729
730 #[test]
731 fn unregister_broker_round_trip() {
732 let r = MetadataRecord::V1UnregisterBroker(UnregisterBrokerRecord {
733 node_id: NodeId(42),
734 broker_epoch: 17,
735 });
736 assert2::assert!(round_trip(&r) == r);
737 }
738
739 #[test]
740 fn broker_registration_change_and_unregister_controller_round_trip() {
741 for r in [
742 MetadataRecord::V1BrokerRegistrationChange(BrokerRegistrationChangeRecord {
743 node_id: NodeId(4),
744 broker_epoch: 12,
745 fenced: FencingChange::Unfence,
746 in_controlled_shutdown: true,
747 log_dirs: vec![Uuid::from_u128(0xD1)],
748 cordoned_log_dirs: Some(vec![]),
749 }),
750 MetadataRecord::V1UnregisterController(UnregisterControllerRecord {
751 node_id: NodeId(3000),
752 }),
753 ] {
754 assert2::assert!(round_trip(&r) == r);
755 }
756 }
757
758 #[test]
761 fn fencing_change_matches_kafka() {
762 for (change, wire, from_false, from_true) in [
763 (FencingChange::Unfence, -1, false, false),
764 (FencingChange::None, 0, false, true),
765 (FencingChange::Fence, 1, true, true),
766 ] {
767 assert2::check!(
768 (
769 change.wire_value(),
770 FencingChange::from_wire(wire),
771 change.apply(false),
772 change.apply(true),
773 ) == (wire, Some(change), from_false, from_true)
774 );
775 }
776 for undefined in [-2, 2, i8::MIN, i8::MAX] {
777 assert2::check!(FencingChange::from_wire(undefined) == None);
778 }
779 }
780
781 #[test]
785 fn registration_change_applies_like_clone_with() {
786 let (d1, d2) = (Uuid::from_u128(0xD1), Uuid::from_u128(0xD2));
787 let current = BrokerRegistrationRecord {
788 node_id: NodeId(4),
789 broker_epoch: 12,
790 incarnation_id: Uuid::from_u128(4),
791 host: "broker-4".into(),
792 port: 9092,
793 rack: None,
794 endpoints: vec![],
795 log_dirs: vec![d1],
796 fenced: true,
797 in_controlled_shutdown: false,
798 cordoned_log_dirs: None,
799 features: std::collections::BTreeMap::new(),
800 };
801 let base = BrokerRegistrationChangeRecord::no_change(NodeId(4), 12);
802 let with = |edit: fn(&mut BrokerRegistrationRecord)| {
803 let mut want = current.clone();
804 edit(&mut want);
805 want
806 };
807 for (case, change, want) in [
808 ("no change", base.clone(), current.clone()),
809 (
810 "unfence",
811 BrokerRegistrationChangeRecord {
812 fenced: FencingChange::Unfence,
813 ..base.clone()
814 },
815 with(|b| b.fenced = false),
816 ),
817 (
818 "controlled shutdown",
819 BrokerRegistrationChangeRecord {
820 in_controlled_shutdown: true,
821 ..base.clone()
822 },
823 with(|b| b.in_controlled_shutdown = true),
824 ),
825 (
826 "new log dirs",
827 BrokerRegistrationChangeRecord {
828 log_dirs: vec![d2],
829 ..base.clone()
830 },
831 with(|b| b.log_dirs = vec![Uuid::from_u128(0xD2)]),
832 ),
833 (
834 "uncordon every dir",
835 BrokerRegistrationChangeRecord {
836 cordoned_log_dirs: Some(vec![]),
837 ..base.clone()
838 },
839 with(|b| b.cordoned_log_dirs = Some(vec![])),
840 ),
841 ] {
842 assert2::check!(change.applied_to(¤t) == want, "{case}");
843 }
844
845 let shutting_down = BrokerRegistrationRecord {
846 in_controlled_shutdown: true,
847 ..current.clone()
848 };
849 assert2::check!(base.applied_to(&shutting_down) == shutting_down);
850 }
851
852 #[test]
853 fn topic_config_record_round_trip() {
854 let mut overrides = std::collections::BTreeMap::new();
855 overrides.insert("retention.ms".to_string(), "60000".to_string());
856 overrides.insert("segment.bytes".to_string(), "1048576".to_string());
857 let r = MetadataRecord::V1TopicConfig(TopicConfigRecord {
858 topic: "t".into(),
859 overrides,
860 });
861 assert2::assert!(round_trip(&r) == r);
862 }
863
864 #[test]
865 fn scram_credential_round_trip() {
866 let r = MetadataRecord::V1ScramCredential(ScramCredentialRecord {
867 user: "alice".into(),
868 mechanism: krabka_security::SaslMechanism::ScramSha512,
869 salt: vec![1u8; 16],
870 stored_key: vec![2u8; 64],
871 server_key: vec![3u8; 64],
872 iterations: 4096,
873 });
874 assert2::assert!(round_trip(&r) == r);
875 }
876
877 #[test]
878 fn delete_scram_credential_round_trip() {
879 let r = MetadataRecord::V1DeleteScramCredential(DeleteScramCredentialRecord {
880 user: "alice".into(),
881 mechanism: krabka_security::SaslMechanism::ScramSha512,
882 });
883 assert2::assert!(round_trip(&r) == r);
884 }
885
886 #[test]
887 fn v1_access_control_entry_round_trip() {
888 let entry = crate::AclEntry {
889 resource_type: crate::ResourceType::Topic,
890 resource_name: "foo".into(),
891 pattern_type: crate::PatternType::Literal,
892 principal: "User:alice".into(),
893 host: "*".into(),
894 operation: crate::AclOperation::Read,
895 permission_type: crate::PermissionType::Allow,
896 };
897 let r = MetadataRecord::V1AccessControlEntry(entry);
898 assert2::assert!(round_trip(&r) == r);
899 }
900
901 #[test]
902 fn v1_delete_access_control_entry_round_trip() {
903 let filter = crate::AclEntryFilter {
904 resource_type: Some(crate::ResourceType::Group),
905 resource_name: Some("cg-foo".into()),
906 pattern_type: Some(crate::PatternType::Literal),
907 principal: None,
908 host: None,
909 operation: None,
910 permission_type: None,
911 };
912 let r = MetadataRecord::V1DeleteAccessControlEntry(filter);
913 assert2::assert!(round_trip(&r) == r);
914 }
915
916 #[test]
917 fn broker_config_record_round_trip() {
918 let r = MetadataRecord::V1BrokerConfig(BrokerConfigRecord {
919 node_id: NodeId(7),
920 config_name: "leader.replication.throttled.rate".into(),
921 config_value: Some("2048".into()),
922 });
923 assert2::assert!(round_trip(&r) == r);
924 }
925
926 #[test]
927 fn client_quota_record_round_trip() {
928 let r = MetadataRecord::V1ClientQuota(ClientQuotaRecord {
929 entity: vec![
930 QuotaEntity {
931 entity_type: "client-id".into(),
932 entity_name: Some("app1".into()),
933 },
934 QuotaEntity {
935 entity_type: "user".into(),
936 entity_name: Some("alice".into()),
937 },
938 ],
939 config_key: "producer_byte_rate".into(),
940 config_value: Some(1024.0),
941 });
942 assert2::assert!(round_trip(&r) == r);
943 }
944
945 #[test]
946 fn delegation_token_record_round_trip() {
947 let r = MetadataRecord::V1DelegationToken(DelegationTokenRecord {
948 token_id: "tok-abc".into(),
949 owner: krabka_security::KafkaPrincipal {
950 principal_type: "User".into(),
951 name: "alice".into(),
952 },
953 requester: krabka_security::KafkaPrincipal {
954 principal_type: "User".into(),
955 name: "admin".into(),
956 },
957 issue_timestamp_ms: 1_700_000_000_000,
958 expiry_timestamp_ms: 1_700_000_600_000,
959 max_timestamp_ms: 1_700_604_800_000,
960 renewers: vec![krabka_security::KafkaPrincipal {
961 principal_type: "User".into(),
962 name: "bob".into(),
963 }],
964 });
965 assert2::assert!(round_trip(&r) == r);
966 }
967
968 #[test]
969 fn delete_delegation_token_record_round_trip() {
970 let r = MetadataRecord::V1DeleteDelegationToken(DeleteDelegationTokenRecord {
971 token_id: "tok-abc".into(),
972 });
973 assert2::assert!(round_trip(&r) == r);
974 }
975
976 #[test]
977 fn voters_record_round_trips() {
978 let rec = MetadataRecord::V1Voters(VotersRecord {
979 voters: crate::voters::VoterSet::from_voters([crate::voters::Voter {
980 id: NodeId(7),
981 directory_id: uuid::Uuid::from_u128(7),
982 endpoints: vec![crate::voters::VoterEndpoint {
983 name: "CONTROLLER".into(),
984 host: "h".into(),
985 port: 1,
986 }],
987 kraft_version: crate::voters::KRaftVersionRange::default(),
988 }]),
989 });
990 assert2::assert!(round_trip(&rec) == rec);
991 }
992
993 #[test]
994 fn kraft_version_record_round_trips() {
995 let rec = MetadataRecord::V1KRaftVersion(KRaftVersionRecord { kraft_version: 1 });
996 assert2::assert!(round_trip(&rec) == rec);
997 }
998
999 #[test]
1000 fn client_metrics_config_round_trip() {
1001 let mut overrides = std::collections::BTreeMap::new();
1002 overrides.insert("interval.ms".to_string(), "60000".to_string());
1003 overrides.insert(
1004 "metrics".to_string(),
1005 "org.apache.kafka.consumer.".to_string(),
1006 );
1007 let r = MetadataRecord::V1ClientMetricsConfig(ClientMetricsConfigRecord {
1008 name: "sub-a".into(),
1009 configs: overrides,
1010 });
1011 assert2::assert!(round_trip(&r) == r);
1012 }
1013
1014 #[test]
1015 fn topic_freeze_round_trip() {
1016 let r = MetadataRecord::V1TopicFreeze(crate::write_freeze::TopicFreezeRecord {
1017 scope: "tenant-a.".into(),
1018 pattern_type: crate::PatternType::Prefixed,
1019 frozen: true,
1020 reason: "DR cutover".into(),
1021 set_by: "User:alice".into(),
1022 set_at_ms: 1_700_000_000_000,
1023 proposal_id: Uuid::from_u128(0xB1),
1024 key_id: "alice-yubi".into(),
1025 signature: vec![0xAB; 64],
1026 });
1027 assert2::assert!(round_trip(&r) == r);
1028 }
1029
1030 #[test]
1031 fn break_glass_proposal_round_trip() {
1032 let r =
1033 MetadataRecord::V1BreakGlassProposal(crate::break_glass::BreakGlassProposalRecord {
1034 proposal_id: Uuid::from_u128(0xB1),
1035 action: crate::break_glass::BreakGlassAction::ThawTopicFreeze,
1036 target: "literal:orders".into(),
1037 proposer: "User:alice".into(),
1038 reason: "incident 4711 closed".into(),
1039 created_at_ms: 1_700_000_000_000,
1040 expires_at_ms: 1_700_000_600_000,
1041 approvals: vec![crate::break_glass::BreakGlassApproval {
1042 principal: "User:bob".into(),
1043 approved_at_ms: 1_700_000_060_000,
1044 key_id: "bob-yubi".into(),
1045 signature: vec![0xCD; 64],
1046 }],
1047 consumed_at_ms: 0,
1048 withdrawn: false,
1049 });
1050 assert2::assert!(round_trip(&r) == r);
1051 }
1052
1053 #[test]
1054 fn delete_break_glass_proposal_round_trip() {
1055 let r = MetadataRecord::V1DeleteBreakGlassProposal(Uuid::from_u128(0xB1));
1056 assert2::assert!(round_trip(&r) == r);
1057 }
1058
1059 #[test]
1060 fn partition_epoch_serde_default_is_minus_one() {
1061 assert2::assert!(default_partition_epoch() == -1);
1064 }
1065}