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