1use std::{collections::BTreeMap, vec};
52
53use bytes::Bytes;
54use krabka_protocol::{
55 owned::{
56 access_control_entry_record::AccessControlEntryRecord,
57 broker_registration_change_record::BrokerRegistrationChangeRecord,
58 client_quota_record::{ClientQuotaRecord as KClientQuotaRecord, EntityData},
59 config_record::ConfigRecord,
60 delegation_token_record::DelegationTokenRecord as KDelegationTokenRecord,
61 feature_level_record::FeatureLevelRecord as KFeatureLevelRecord,
62 no_op_record::NoOpRecord,
63 partition_change_record::PartitionChangeRecord as KPartitionChangeRecord,
64 partition_record::PartitionRecord as KPartitionRecord,
65 producer_ids_record::ProducerIdsRecord as KProducerIdsRecord,
66 register_broker_record::{
67 BrokerEndpoint as KBrokerEndpoint, BrokerFeature as KBrokerFeature,
68 RegisterBrokerRecord,
69 },
70 register_controller_record::{
71 ControllerEndpoint as KControllerEndpoint, ControllerFeature as KControllerFeature,
72 RegisterControllerRecord,
73 },
74 remove_access_control_entry_record::RemoveAccessControlEntryRecord,
75 remove_delegation_token_record::RemoveDelegationTokenRecord,
76 remove_topic_record::RemoveTopicRecord,
77 remove_user_scram_credential_record::RemoveUserScramCredentialRecord,
78 topic_record::TopicRecord as KTopicRecord,
79 unregister_broker_record::UnregisterBrokerRecord as KUnregisterBrokerRecord,
80 unregister_controller_record::UnregisterControllerRecord as KUnregisterControllerRecord,
81 user_scram_credential_record::UserScramCredentialRecord,
82 },
83 primitives::uuid::Uuid as KUuid,
84 records::metadata::KraftMetadataRecord,
85 tagged_fields::{UnknownTaggedField, UnknownTaggedFields},
86};
87use krabka_security::{KafkaPrincipal, ListenerProtocol, SaslMechanism};
88use wincode::{SchemaRead, Serialize as _, config::DefaultConfig};
89
90use crate::{
91 MetadataImage,
92 acl::{AclEntry, AclEntryFilter, AclOperation, PatternType, PermissionType, ResourceType},
93 directory_id::MIGRATING,
94 metadata_version::{
95 CORDONED_LOG_DIRS_MIN_LEVEL, DIRECTORY_ASSIGNMENT_MIN_LEVEL, ELR_MIN_LEVEL,
96 METADATA_VERSION_MAX,
97 },
98 records::{
99 BrokerConfigRecord, BrokerEndpoint, BrokerRegistrationChangeRecord as ChangeRecord,
100 BrokerRegistrationRecord, ClientMetricsConfigRecord, ClientQuotaRecord,
101 ControllerRegistrationRecord, DEFAULT_BROKER_CONFIG_NODE_ID, DelegationTokenRecord,
102 DeleteDelegationTokenRecord, DeleteScramCredentialRecord, DeleteTopicRecord,
103 FeatureLevelRecord, FencingChange, GroupConfigRecord, LeaderEpoch, LeaderRecoveryState,
104 MetadataRecord, NodeId, PartitionDirAssignmentRecord, PartitionRecord, ProducerIdsRecord,
105 QuotaEntity, ScramCredentialRecord, TopicConfigRecord, TopicRecord, UnregisterBrokerRecord,
106 UnregisterControllerRecord,
107 },
108};
109
110#[derive(Debug, thiserror::Error, PartialEq, Eq)]
111pub enum TranslateError {
112 #[error("record has no KIP-631 metadata counterpart: {0}")]
113 NoCounterpart(&'static str),
114 #[error("unknown topic id {0} on decode")]
115 UnknownTopicId(uuid::Uuid),
116 #[error("unknown topic name {0} on encode")]
117 UnknownTopicName(String),
118 #[error("invalid reference in {field}: {detail}")]
126 InvalidReference { field: &'static str, detail: String },
127 #[error("invalid {field}: {detail}")]
134 InvalidValue { field: &'static str, detail: String },
135 #[error("KIP-631 value encode failed: {0}")]
136 Encode(String),
137 #[error("KIP-631 value decode failed: {0}")]
138 Decode(String),
139 #[error("unknown ACL id {0} on decode")]
143 UnknownAclId(uuid::Uuid),
144 #[error("unknown krabka-private NoOpRecord tag {0}")]
148 UnknownPrivateTag(u32),
149 #[error(
153 "krabka-private NoOpRecord tag {tag} has record version {version}, and this build reads \
154 only version {PRIVATE_RECORD_VERSION}; a record without a version predates krabka 1.0, \
155 and its node must be reformatted"
156 )]
157 UnknownPrivateRecordVersion { tag: u32, version: i16 },
158 #[error(
161 "krabka-private NoOpRecord tag {tag} carries a record that belongs to tag {expected_tag:?}"
162 )]
163 PrivateTagMismatch { tag: u32, expected_tag: Option<u32> },
164 #[error("krabka-private NoOpRecord tag {tag} has {trailing} bytes after its record")]
167 TrailingPrivateRecordBytes { tag: u32, trailing: usize },
168}
169
170fn to_kuuid(u: uuid::Uuid) -> KUuid {
173 KUuid(*u.as_bytes())
174}
175
176fn from_kuuid(u: KUuid) -> uuid::Uuid {
177 uuid::Uuid::from_bytes(u.0)
178}
179
180fn scram_mechanism_to_wire(m: SaslMechanism) -> Result<i8, TranslateError> {
183 match m {
184 SaslMechanism::ScramSha256 => Ok(1),
185 SaslMechanism::ScramSha512 => Ok(2),
186 other => Err(TranslateError::InvalidValue {
187 field: "scram mechanism",
188 detail: format!("{other:?} is not a SCRAM mechanism"),
189 }),
190 }
191}
192
193fn scram_mechanism_from_wire(b: i8) -> Result<SaslMechanism, TranslateError> {
194 match b {
195 1 => Ok(SaslMechanism::ScramSha256),
196 2 => Ok(SaslMechanism::ScramSha512),
197 other => Err(TranslateError::InvalidValue {
198 field: "scram mechanism",
199 detail: format!("unknown SCRAM mechanism wire byte {other}"),
200 }),
201 }
202}
203
204fn resource_type_to_wire(rt: ResourceType) -> i8 {
211 match rt {
212 ResourceType::Topic => 2,
213 ResourceType::Group => 3,
214 ResourceType::Cluster => 4,
215 ResourceType::TransactionalId => 5,
216 ResourceType::DelegationToken => 6,
217 ResourceType::User => 7,
218 }
219}
220
221fn resource_type_from_wire(b: i8) -> Result<ResourceType, TranslateError> {
222 match b {
223 2 => Ok(ResourceType::Topic),
224 3 => Ok(ResourceType::Group),
225 4 => Ok(ResourceType::Cluster),
226 5 => Ok(ResourceType::TransactionalId),
227 6 => Ok(ResourceType::DelegationToken),
228 7 => Ok(ResourceType::User),
229 other => Err(TranslateError::InvalidValue {
230 field: "acl resource_type",
231 detail: format!("unknown wire byte {other}"),
232 }),
233 }
234}
235
236fn pattern_type_to_wire(pt: PatternType) -> i8 {
237 match pt {
238 PatternType::Literal => 3,
239 PatternType::Prefixed => 4,
240 }
241}
242
243fn pattern_type_from_wire(b: i8) -> Result<PatternType, TranslateError> {
244 match b {
245 3 => Ok(PatternType::Literal),
246 4 => Ok(PatternType::Prefixed),
247 other => Err(TranslateError::InvalidValue {
248 field: "acl pattern_type",
249 detail: format!("unknown wire byte {other}"),
250 }),
251 }
252}
253
254fn operation_to_wire(op: AclOperation) -> i8 {
255 match op {
256 AclOperation::All => 2,
257 AclOperation::Read => 3,
258 AclOperation::Write => 4,
259 AclOperation::Create => 5,
260 AclOperation::Delete => 6,
261 AclOperation::Alter => 7,
262 AclOperation::Describe => 8,
263 AclOperation::ClusterAction => 9,
264 AclOperation::DescribeConfigs => 10,
265 AclOperation::AlterConfigs => 11,
266 AclOperation::IdempotentWrite => 12,
267 AclOperation::CreateTokens => 13,
268 AclOperation::DescribeTokens => 14,
269 AclOperation::TwoPhaseCommit => 15,
270 }
271}
272
273fn operation_from_wire(b: i8) -> Result<AclOperation, TranslateError> {
274 match b {
275 2 => Ok(AclOperation::All),
276 3 => Ok(AclOperation::Read),
277 4 => Ok(AclOperation::Write),
278 5 => Ok(AclOperation::Create),
279 6 => Ok(AclOperation::Delete),
280 7 => Ok(AclOperation::Alter),
281 8 => Ok(AclOperation::Describe),
282 9 => Ok(AclOperation::ClusterAction),
283 10 => Ok(AclOperation::DescribeConfigs),
284 11 => Ok(AclOperation::AlterConfigs),
285 12 => Ok(AclOperation::IdempotentWrite),
286 13 => Ok(AclOperation::CreateTokens),
287 14 => Ok(AclOperation::DescribeTokens),
288 15 => Ok(AclOperation::TwoPhaseCommit),
289 other => Err(TranslateError::InvalidValue {
290 field: "acl operation",
291 detail: format!("unknown wire byte {other}"),
292 }),
293 }
294}
295
296fn permission_to_wire(pt: PermissionType) -> i8 {
297 match pt {
298 PermissionType::Deny => 2,
299 PermissionType::Allow => 3,
300 }
301}
302
303fn permission_from_wire(b: i8) -> Result<PermissionType, TranslateError> {
304 match b {
305 2 => Ok(PermissionType::Deny),
306 3 => Ok(PermissionType::Allow),
307 other => Err(TranslateError::InvalidValue {
308 field: "acl permission_type",
309 detail: format!("unknown wire byte {other}"),
310 }),
311 }
312}
313
314fn acl_id(e: &AclEntry) -> KUuid {
326 let mut buf: Vec<u8> = vec![
329 resource_type_to_wire(e.resource_type).cast_unsigned(),
330 pattern_type_to_wire(e.pattern_type).cast_unsigned(),
331 operation_to_wire(e.operation).cast_unsigned(),
332 permission_to_wire(e.permission_type).cast_unsigned(),
333 ];
334 for s in [&e.resource_name, &e.principal, &e.host] {
335 buf.extend_from_slice(&(s.len() as u64).to_le_bytes());
336 buf.extend_from_slice(s.as_bytes());
337 }
338 KUuid(fnv1a_128(&buf))
339}
340
341fn fnv1a_128(bytes: &[u8]) -> [u8; 16] {
345 const PRIME: u64 = 0x0000_0100_0000_01b3;
346 let lane = |mut h: u64| -> u64 {
347 for b in bytes {
348 h ^= u64::from(*b);
349 h = h.wrapping_mul(PRIME);
350 }
351 h
352 };
353 let hi = lane(0xcbf2_9ce4_8422_2325);
354 let lo = lane(0x1099_5663_8a1b_c7e9);
355 let mut out = [0u8; 16];
356 out[..8].copy_from_slice(&hi.to_be_bytes());
357 out[8..].copy_from_slice(&lo.to_be_bytes());
358 out
359}
360
361fn exact_filter(e: &AclEntry) -> AclEntryFilter {
365 AclEntryFilter {
366 resource_type: Some(e.resource_type),
367 resource_name: Some(e.resource_name.clone()),
368 pattern_type: Some(e.pattern_type),
369 principal: Some(e.principal.clone()),
370 host: Some(e.host.clone()),
371 operation: Some(e.operation),
372 permission_type: Some(e.permission_type),
373 }
374}
375
376fn protocol_to_wire(p: ListenerProtocol) -> i16 {
379 match p {
380 ListenerProtocol::Plaintext => 0,
381 ListenerProtocol::Ssl => 1,
382 ListenerProtocol::SaslPlaintext => 2,
383 ListenerProtocol::SaslSsl => 3,
384 }
385}
386
387fn protocol_from_wire(b: i16) -> Result<ListenerProtocol, TranslateError> {
388 match b {
389 0 => Ok(ListenerProtocol::Plaintext),
390 1 => Ok(ListenerProtocol::Ssl),
391 2 => Ok(ListenerProtocol::SaslPlaintext),
392 3 => Ok(ListenerProtocol::SaslSsl),
393 other => Err(TranslateError::InvalidValue {
394 field: "security_protocol",
395 detail: format!("unknown wire value {other}"),
396 }),
397 }
398}
399
400pub fn to_kraft(
414 rec: &MetadataRecord,
415 image: &MetadataImage,
416) -> Result<KraftMetadataRecord, TranslateError> {
417 to_kraft_iter(rec, image)?
418 .next()
419 .ok_or(TranslateError::NoCounterpart("empty translation"))
420}
421
422pub fn to_kraft_records(
431 rec: &MetadataRecord,
432 image: &MetadataImage,
433) -> Result<Vec<KraftMetadataRecord>, TranslateError> {
434 Ok(to_kraft_iter(rec, image)?.collect())
435}
436
437pub fn to_kraft_values(
463 rec: &MetadataRecord,
464 image: &MetadataImage,
465) -> Result<Vec<Bytes>, TranslateError> {
466 let metadata_version = image
467 .finalized_metadata_version()
468 .unwrap_or(METADATA_VERSION_MAX);
469 to_kraft_records(rec, image)?
470 .into_iter()
471 .map(|mut kr| {
472 if let KraftMetadataRecord::BrokerRegistrationChange(change) = &mut kr {
475 if metadata_version < DIRECTORY_ASSIGNMENT_MIN_LEVEL {
476 change.log_dirs.clear();
477 }
478 if metadata_version < CORDONED_LOG_DIRS_MIN_LEVEL {
479 change.cordoned_log_dirs = None;
480 }
481 }
482 let version: i16 = match &kr {
483 KraftMetadataRecord::BrokerRegistrationChange(change) => {
484 broker_registration_change_version(change)
485 }
486 KraftMetadataRecord::Partition(_) | KraftMetadataRecord::PartitionChange(_) => {
487 match metadata_version {
488 version if version >= ELR_MIN_LEVEL => 2,
489 version if version >= DIRECTORY_ASSIGNMENT_MIN_LEVEL => 1,
490 _ => 0,
491 }
492 }
493 KraftMetadataRecord::RegisterBroker(_) => {
494 if metadata_version >= CORDONED_LOG_DIRS_MIN_LEVEL {
495 4
496 } else if metadata_version >= DIRECTORY_ASSIGNMENT_MIN_LEVEL {
497 3
498 } else if metadata_version >= 8 {
499 2
500 } else {
501 1
502 }
503 }
504 _ => 0,
506 };
507 if let KraftMetadataRecord::RegisterBroker(register) = &mut kr {
514 if version < 3 {
515 register.log_dirs.clear();
516 }
517 if version < 4 {
518 register.cordoned_log_dirs = None;
519 }
520 }
521 kr.encode_value(version)
522 .map_err(|e| TranslateError::Encode(e.to_string()))
523 })
524 .collect()
525}
526
527pub fn from_kraft_value(
540 value: &[u8],
541 image: &MetadataImage,
542) -> Result<MetadataRecord, TranslateError> {
543 let (rec, _version) = KraftMetadataRecord::decode_value(value)
544 .map_err(|e| TranslateError::Decode(e.to_string()))?;
545 from_kraft(&rec, image)
546}
547
548fn topic_config_to_kraft(
549 config: &TopicConfigRecord,
550 image: &MetadataImage,
551) -> Vec<KraftMetadataRecord> {
552 let empty = BTreeMap::new();
553 let old = image.topic_config(&config.topic).unwrap_or(&empty);
554 let mut output = Vec::with_capacity(config.overrides.len());
555 for (key, value) in &config.overrides {
556 output.push(KraftMetadataRecord::Config(ConfigRecord {
557 resource_type: 2,
558 resource_name: config.topic.clone(),
559 name: key.clone(),
560 value: Some(value.clone()),
561 ..Default::default()
562 }));
563 }
564 for key in old.keys() {
565 if !config.overrides.contains_key(key) {
566 output.push(KraftMetadataRecord::Config(ConfigRecord {
567 resource_type: 2,
568 resource_name: config.topic.clone(),
569 name: key.clone(),
570 value: None,
571 ..Default::default()
572 }));
573 }
574 }
575 output
576}
577
578fn group_config_to_kraft(
579 config: &GroupConfigRecord,
580 image: &MetadataImage,
581) -> Vec<KraftMetadataRecord> {
582 let empty = BTreeMap::new();
583 let old = image.group_config(&config.group_id).unwrap_or(&empty);
584 let mut output = Vec::with_capacity(config.configs.len());
585 for (key, value) in &config.configs {
586 output.push(KraftMetadataRecord::Config(ConfigRecord {
587 resource_type: 32,
588 resource_name: config.group_id.clone(),
589 name: key.clone(),
590 value: Some(value.clone()),
591 ..Default::default()
592 }));
593 }
594 for key in old.keys() {
595 if !config.configs.contains_key(key) {
596 output.push(KraftMetadataRecord::Config(ConfigRecord {
597 resource_type: 32,
598 resource_name: config.group_id.clone(),
599 name: key.clone(),
600 value: None,
601 ..Default::default()
602 }));
603 }
604 }
605 output
606}
607
608fn client_metrics_config_to_kraft(
609 config: &ClientMetricsConfigRecord,
610 image: &MetadataImage,
611) -> Vec<KraftMetadataRecord> {
612 let empty = BTreeMap::new();
613 let old = image.client_metrics_config(&config.name).unwrap_or(&empty);
614 let mut output = Vec::with_capacity(config.configs.len());
615 for (key, value) in &config.configs {
616 output.push(KraftMetadataRecord::Config(ConfigRecord {
617 resource_type: 16,
618 resource_name: config.name.clone(),
619 name: key.clone(),
620 value: Some(value.clone()),
621 ..Default::default()
622 }));
623 }
624 for key in old.keys() {
625 if !config.configs.contains_key(key) {
626 output.push(KraftMetadataRecord::Config(ConfigRecord {
627 resource_type: 16,
628 resource_name: config.name.clone(),
629 name: key.clone(),
630 value: None,
631 ..Default::default()
632 }));
633 }
634 }
635 output
636}
637
638fn delete_acl_to_kraft(
639 filter: &crate::AclEntryFilter,
640 image: &MetadataImage,
641) -> Vec<KraftMetadataRecord> {
642 image
643 .all_acls()
644 .filter(|entry| filter.matches(entry))
645 .map(|entry| {
646 KraftMetadataRecord::RemoveAccessControlEntry(RemoveAccessControlEntryRecord {
647 id: acl_id(entry),
648 ..Default::default()
649 })
650 })
651 .collect()
652}
653
654fn acl_to_kraft(entry: &crate::AclEntry) -> KraftMetadataRecord {
655 KraftMetadataRecord::AccessControlEntry(AccessControlEntryRecord {
656 id: acl_id(entry),
657 resource_type: resource_type_to_wire(entry.resource_type),
658 resource_name: entry.resource_name.clone(),
659 pattern_type: pattern_type_to_wire(entry.pattern_type),
660 principal: entry.principal.clone(),
661 host: entry.host.clone(),
662 operation: operation_to_wire(entry.operation),
663 permission_type: permission_to_wire(entry.permission_type),
664 ..Default::default()
665 })
666}
667
668fn scram_to_kraft(record: &MetadataRecord) -> Result<Vec<KraftMetadataRecord>, TranslateError> {
669 match record {
670 MetadataRecord::V1ScramCredential(scram) => {
671 let iterations =
672 i32::try_from(scram.iterations).map_err(|_| TranslateError::InvalidValue {
673 field: "scram iterations",
674 detail: format!("{} exceeds i32", scram.iterations),
675 })?;
676 Ok(vec![KraftMetadataRecord::UserScramCredential(
677 UserScramCredentialRecord {
678 name: scram.user.clone(),
679 mechanism: scram_mechanism_to_wire(scram.mechanism)?,
680 salt: bytes::Bytes::copy_from_slice(&scram.salt),
681 stored_key: bytes::Bytes::copy_from_slice(&scram.stored_key),
682 server_key: bytes::Bytes::copy_from_slice(&scram.server_key),
683 iterations,
684 ..Default::default()
685 },
686 )])
687 }
688 MetadataRecord::V1DeleteScramCredential(scram) => {
689 Ok(vec![KraftMetadataRecord::RemoveUserScramCredential(
690 RemoveUserScramCredentialRecord {
691 name: scram.user.clone(),
692 mechanism: scram_mechanism_to_wire(scram.mechanism)?,
693 ..Default::default()
694 },
695 )])
696 }
697 _ => unreachable!("non-SCRAM record routed to scram_to_kraft"),
698 }
699}
700
701fn to_kraft_iter(
703 rec: &MetadataRecord,
704 image: &MetadataImage,
705) -> Result<vec::IntoIter<KraftMetadataRecord>, TranslateError> {
706 let recs: Vec<KraftMetadataRecord> = match rec {
707 MetadataRecord::V1FeatureLevel(f) => {
708 vec![KraftMetadataRecord::FeatureLevel(KFeatureLevelRecord {
709 name: f.name.clone(),
710 feature_level: f.level,
711 ..Default::default()
712 })]
713 }
714 MetadataRecord::V1BrokerRegistration(b) => {
715 vec![KraftMetadataRecord::RegisterBroker(
716 register_broker_to_kraft(b)?,
717 )]
718 }
719 MetadataRecord::V1ControllerRegistration(c) => {
720 vec![KraftMetadataRecord::RegisterController(
721 register_controller_to_kraft(c)?,
722 )]
723 }
724 MetadataRecord::V1UnregisterBroker(u) => {
725 vec![KraftMetadataRecord::UnregisterBroker(
726 KUnregisterBrokerRecord {
727 broker_id: node_id_to_wire(u.node_id, "broker_id")?,
728 broker_epoch: u.broker_epoch,
729 ..Default::default()
730 },
731 )]
732 }
733 MetadataRecord::V1BrokerRegistrationChange(change) => {
734 vec![KraftMetadataRecord::BrokerRegistrationChange(
735 broker_registration_change_to_kraft(change)?,
736 )]
737 }
738 MetadataRecord::V1UnregisterController(u) => {
739 vec![KraftMetadataRecord::UnregisterController(
740 KUnregisterControllerRecord {
741 controller_id: node_id_to_wire(u.node_id, "controller_id")?,
742 ..Default::default()
743 },
744 )]
745 }
746 MetadataRecord::V1ScramCredential(_) | MetadataRecord::V1DeleteScramCredential(_) => {
747 scram_to_kraft(rec)?
748 }
749 MetadataRecord::V1AccessControlEntry(entry) => vec![acl_to_kraft(entry)],
750 MetadataRecord::V1ClientQuota(q) => {
751 vec![KraftMetadataRecord::ClientQuota(client_quota_to_kraft(q))]
752 }
753 MetadataRecord::V1DelegationToken(t) => {
754 vec![KraftMetadataRecord::DelegationToken(
755 delegation_token_to_kraft(t),
756 )]
757 }
758 MetadataRecord::V1DeleteDelegationToken(t) => {
759 vec![KraftMetadataRecord::RemoveDelegationToken(
760 RemoveDelegationTokenRecord {
761 token_id: t.token_id.clone(),
762 ..Default::default()
763 },
764 )]
765 }
766 MetadataRecord::V1BrokerConfig(c) => {
767 vec![KraftMetadataRecord::Config(ConfigRecord {
768 resource_type: 4, resource_name: if c.node_id == DEFAULT_BROKER_CONFIG_NODE_ID {
770 String::new()
771 } else {
772 c.node_id.to_string()
773 },
774 name: c.config_name.clone(),
775 value: c.config_value.clone(),
776 ..Default::default()
777 })]
778 }
779 MetadataRecord::V1Topic(t) => {
781 vec![KraftMetadataRecord::Topic(KTopicRecord {
782 name: t.name.clone(),
783 topic_id: to_kuuid(t.topic_id),
784 ..Default::default()
785 })]
786 }
787 MetadataRecord::V1Partition(p) => {
788 let topic = image
789 .topic(&p.topic)
790 .ok_or_else(|| TranslateError::UnknownTopicName(p.topic.clone()))?;
791 vec![KraftMetadataRecord::Partition(partition_to_kraft(
792 p,
793 topic.topic_id,
794 image,
795 )?)]
796 }
797 MetadataRecord::V1DeleteTopic(d) => {
798 let topic = image
799 .topic(&d.name)
800 .ok_or_else(|| TranslateError::UnknownTopicName(d.name.clone()))?;
801 vec![KraftMetadataRecord::RemoveTopic(RemoveTopicRecord {
802 topic_id: to_kuuid(topic.topic_id),
803 ..Default::default()
804 })]
805 }
806 MetadataRecord::V1TopicConfig(c) => topic_config_to_kraft(c, image),
807 MetadataRecord::V1GroupConfig(c) => group_config_to_kraft(c, image),
808 MetadataRecord::V1ProducerIds(record) => {
809 vec![KraftMetadataRecord::ProducerIds(KProducerIdsRecord {
810 broker_id: i32::try_from(record.broker_id.0).map_err(|_| {
811 TranslateError::InvalidValue {
812 field: "producer IDs broker id",
813 detail: format!("node_id {} exceeds i32", record.broker_id),
814 }
815 })?,
816 broker_epoch: record.broker_epoch,
817 next_producer_id: record.next_producer_id,
818 ..KProducerIdsRecord::default()
819 })]
820 }
821 MetadataRecord::V1DeleteAccessControlEntry(filter) => delete_acl_to_kraft(filter, image),
822 MetadataRecord::V1ClientMetricsConfig(config) => {
824 client_metrics_config_to_kraft(config, image)
825 }
826 MetadataRecord::V1PartitionDirAssignment(r) => {
830 vec![KraftMetadataRecord::PartitionChange(
831 partition_dir_assignment_to_kraft(r, image)?,
832 )]
833 }
834 MetadataRecord::V1PartitionElr(r) => vec![KraftMetadataRecord::PartitionChange(
835 partition_state_to_kraft(
836 image,
837 &r.topic,
838 r.partition,
839 -1,
840 Some(&r.eligible_leader_replicas),
841 Some(&r.last_known_elr),
842 )?,
843 )],
844 MetadataRecord::V1PartitionRecovery(r) => vec![KraftMetadataRecord::PartitionChange(
845 partition_state_to_kraft(image, &r.topic, r.partition, r.state as i8, None, None)?,
846 )],
847 MetadataRecord::V1PartitionUpdate(r) => vec![KraftMetadataRecord::PartitionChange(
848 partition_update_to_kraft(r, image)?,
849 )],
850 MetadataRecord::V1FeaturesEpoch(_)
861 | MetadataRecord::V1PartitionOffsetAdvance(_)
862 | MetadataRecord::V1TopicFreeze(_)
863 | MetadataRecord::V1BreakGlassProposal(_)
864 | MetadataRecord::V1DeleteBreakGlassProposal(_) => vec![private_carrier(rec)?],
865 MetadataRecord::V1Voters(_) => {
867 return Err(TranslateError::NoCounterpart("V1Voters"));
868 }
869 MetadataRecord::V1KRaftVersion(_) => {
870 return Err(TranslateError::NoCounterpart("V1KRaftVersion"));
871 }
872 };
873 Ok(recs.into_iter())
874}
875
876const PRIVATE_FEATURES_EPOCH_TAG: u32 = 1001;
910const PRIVATE_PARTITION_OFFSET_ADVANCE_TAG: u32 = 1003;
913const PRIVATE_TOPIC_FREEZE_TAG: u32 = 1004;
917const PRIVATE_BREAK_GLASS_PROPOSAL_TAG: u32 = 1005;
920const PRIVATE_DELETE_BREAK_GLASS_PROPOSAL_TAG: u32 = 1006;
922
923const PRIVATE_TAG_FLOOR: u32 = 1001;
927
928const PRIVATE_TAGS: [u32; 5] = [
930 PRIVATE_FEATURES_EPOCH_TAG,
931 PRIVATE_PARTITION_OFFSET_ADVANCE_TAG,
932 PRIVATE_TOPIC_FREEZE_TAG,
933 PRIVATE_BREAK_GLASS_PROPOSAL_TAG,
934 PRIVATE_DELETE_BREAK_GLASS_PROPOSAL_TAG,
935];
936
937pub const PRIVATE_RECORD_VERSION: i16 = 0;
941
942fn private_tag(rec: &MetadataRecord) -> Option<u32> {
946 match rec {
947 MetadataRecord::V1FeaturesEpoch(_) => Some(PRIVATE_FEATURES_EPOCH_TAG),
948 MetadataRecord::V1PartitionOffsetAdvance(_) => Some(PRIVATE_PARTITION_OFFSET_ADVANCE_TAG),
949 MetadataRecord::V1TopicFreeze(_) => Some(PRIVATE_TOPIC_FREEZE_TAG),
950 MetadataRecord::V1BreakGlassProposal(_) => Some(PRIVATE_BREAK_GLASS_PROPOSAL_TAG),
951 MetadataRecord::V1DeleteBreakGlassProposal(_) => {
952 Some(PRIVATE_DELETE_BREAK_GLASS_PROPOSAL_TAG)
953 }
954 _ => None,
955 }
956}
957
958fn private_carrier(rec: &MetadataRecord) -> Result<KraftMetadataRecord, TranslateError> {
966 let tag = private_tag(rec).ok_or(TranslateError::NoCounterpart("private carrier"))?;
967 let record = <serde_wincode::SerdeCompat<MetadataRecord>>::serialize(rec)
968 .map_err(|e| TranslateError::Encode(e.to_string()))?;
969 let mut body = Vec::with_capacity(2 + record.len());
970 body.extend_from_slice(&PRIVATE_RECORD_VERSION.to_be_bytes());
971 body.extend_from_slice(&record);
972 Ok(KraftMetadataRecord::NoOp(NoOpRecord {
973 unknown_tagged_fields: UnknownTaggedFields(vec![UnknownTaggedField {
974 tag,
975 bytes: Bytes::from(body),
976 }]),
977 }))
978}
979
980fn from_private_carrier(no_op: &NoOpRecord) -> Result<MetadataRecord, TranslateError> {
998 let [field] = no_op.unknown_tagged_fields.0.as_slice() else {
999 return Err(TranslateError::NoCounterpart("NoOp"));
1000 };
1001 let tag = field.tag;
1002 if tag < PRIVATE_TAG_FLOOR {
1003 return Err(TranslateError::NoCounterpart("NoOp"));
1004 }
1005 if !PRIVATE_TAGS.contains(&tag) {
1006 return Err(TranslateError::UnknownPrivateTag(tag));
1007 }
1008 let Some((version, record)) = field.bytes.split_first_chunk::<2>() else {
1009 return Err(TranslateError::Decode(format!(
1010 "krabka-private NoOpRecord tag {tag} body is {} bytes, too short for its i16 version",
1011 field.bytes.len()
1012 )));
1013 };
1014 let version = i16::from_be_bytes(*version);
1015 if version != PRIVATE_RECORD_VERSION {
1016 return Err(TranslateError::UnknownPrivateRecordVersion { tag, version });
1017 }
1018 let mut rest: &[u8] = record;
1019 let rec = <serde_wincode::SerdeCompat<MetadataRecord> as SchemaRead<'_, DefaultConfig>>::get(
1020 &mut rest,
1021 )
1022 .map_err(|e| TranslateError::Decode(e.to_string()))?;
1023 if !rest.is_empty() {
1024 return Err(TranslateError::TrailingPrivateRecordBytes {
1025 tag,
1026 trailing: rest.len(),
1027 });
1028 }
1029 let expected_tag = private_tag(&rec);
1030 if expected_tag != Some(tag) {
1031 return Err(TranslateError::PrivateTagMismatch { tag, expected_tag });
1032 }
1033 Ok(rec)
1034}
1035
1036fn register_broker_to_kraft(
1037 b: &BrokerRegistrationRecord,
1038) -> Result<RegisterBrokerRecord, TranslateError> {
1039 let end_points = if b.endpoints.is_empty() {
1044 vec![KBrokerEndpoint {
1045 host: b.host.clone(),
1046 port: b.port,
1047 ..Default::default()
1048 }]
1049 } else {
1050 b.endpoints
1051 .iter()
1052 .map(|e| KBrokerEndpoint {
1053 name: e.name.clone(),
1054 host: e.host.clone(),
1055 port: e.port,
1056 security_protocol: protocol_to_wire(e.protocol),
1057 ..Default::default()
1058 })
1059 .collect()
1060 };
1061 let features = b
1062 .features
1063 .iter()
1064 .map(
1065 |(name, &(min_supported_version, max_supported_version))| KBrokerFeature {
1066 name: name.clone(),
1067 min_supported_version,
1068 max_supported_version,
1069 ..Default::default()
1070 },
1071 )
1072 .collect();
1073 Ok(RegisterBrokerRecord {
1074 broker_id: i32::try_from(b.node_id.0).map_err(|_| TranslateError::InvalidValue {
1075 field: "broker_id",
1076 detail: format!("node_id {} exceeds i32", b.node_id),
1077 })?,
1078 rack: b.rack.clone(),
1079 end_points,
1080 features,
1081 log_dirs: b.log_dirs.iter().copied().map(to_kuuid).collect(),
1082 cordoned_log_dirs: b
1083 .cordoned_log_dirs
1084 .as_ref()
1085 .map(|dirs| dirs.iter().copied().map(to_kuuid).collect()),
1086 broker_epoch: b.broker_epoch,
1087 incarnation_id: to_kuuid(b.incarnation_id),
1088 fenced: b.fenced,
1089 in_controlled_shutdown: b.in_controlled_shutdown,
1090 ..Default::default()
1091 })
1092}
1093
1094fn register_controller_to_kraft(
1095 controller: &ControllerRegistrationRecord,
1096) -> Result<RegisterControllerRecord, TranslateError> {
1097 let controller_id =
1098 i32::try_from(controller.node_id.0).map_err(|_| TranslateError::InvalidValue {
1099 field: "controller_id",
1100 detail: format!("node_id {} exceeds i32", controller.node_id),
1101 })?;
1102 let end_points = controller
1103 .endpoints
1104 .iter()
1105 .map(|endpoint| KControllerEndpoint {
1106 name: endpoint.name.clone(),
1107 host: endpoint.host.clone(),
1108 port: endpoint.port,
1109 security_protocol: protocol_to_wire(endpoint.protocol),
1110 ..Default::default()
1111 })
1112 .collect();
1113 let features = controller
1114 .features
1115 .iter()
1116 .map(
1117 |(name, &(min_supported_version, max_supported_version))| KControllerFeature {
1118 name: name.clone(),
1119 min_supported_version,
1120 max_supported_version,
1121 ..Default::default()
1122 },
1123 )
1124 .collect();
1125 Ok(RegisterControllerRecord {
1126 controller_id,
1127 incarnation_id: to_kuuid(controller.incarnation_id),
1128 zk_migration_ready: controller.zk_migration_ready,
1129 end_points,
1130 features,
1131 ..Default::default()
1132 })
1133}
1134
1135fn partition_to_kraft(
1136 p: &PartitionRecord,
1137 topic_id: uuid::Uuid,
1138 image: &MetadataImage,
1139) -> Result<KPartitionRecord, TranslateError> {
1140 let cast = |v: &[NodeId], field: &'static str| -> Result<Vec<i32>, TranslateError> {
1141 v.iter()
1142 .map(|n| {
1143 i32::try_from(n.0).map_err(|_| TranslateError::InvalidValue {
1144 field,
1145 detail: format!("node id {n} exceeds i32"),
1146 })
1147 })
1148 .collect()
1149 };
1150 let (eligible, last_known) = image.partition_elr(&p.topic, p.partition);
1151 let has_elr = !eligible.is_empty() || !last_known.is_empty();
1152 Ok(KPartitionRecord {
1153 partition_id: p.partition,
1154 topic_id: to_kuuid(topic_id),
1155 replicas: cast(&p.replicas, "partition replicas")?,
1156 isr: cast(&p.isr, "partition isr")?,
1157 removing_replicas: cast(&p.removing_replicas, "partition removing_replicas")?,
1158 adding_replicas: cast(&p.adding_replicas, "partition adding_replicas")?,
1159 leader: partition_leader_to_wire(p.leader, "partition leader")?,
1160 leader_epoch: p.leader_epoch.get(),
1163 partition_epoch: p.partition_epoch,
1164 directories: p.directories.iter().map(|u| to_kuuid(*u)).collect(),
1166 leader_recovery_state: image.leader_recovery_state(&p.topic, p.partition) as i8,
1167 eligible_leader_replicas: has_elr
1168 .then(|| cast(eligible, "eligible leader replica"))
1169 .transpose()?,
1170 last_known_elr: has_elr
1171 .then(|| cast(last_known, "last known eligible leader replica"))
1172 .transpose()?,
1173 ..Default::default()
1174 })
1175}
1176
1177fn partition_dir_assignment_to_kraft(
1178 assignment: &PartitionDirAssignmentRecord,
1179 image: &MetadataImage,
1180) -> Result<KPartitionChangeRecord, TranslateError> {
1181 let topic = image
1182 .topic(&assignment.topic)
1183 .ok_or_else(|| TranslateError::UnknownTopicName(assignment.topic.clone()))?;
1184 let partition = image
1185 .partition(&assignment.topic, assignment.partition)
1186 .ok_or_else(|| TranslateError::InvalidReference {
1187 field: "partition directory assignment",
1188 detail: format!(
1189 "unknown partition {}-{}",
1190 assignment.topic, assignment.partition
1191 ),
1192 })?;
1193 let replica_slot = partition
1194 .replicas
1195 .iter()
1196 .position(|replica| *replica == assignment.replica)
1197 .ok_or_else(|| TranslateError::InvalidReference {
1198 field: "partition directory assignment",
1199 detail: format!(
1200 "broker {} is not a replica of {}-{}",
1201 assignment.replica, assignment.topic, assignment.partition
1202 ),
1203 })?;
1204
1205 let mut directories = partition.directories.clone();
1208 directories.resize(partition.replicas.len(), MIGRATING);
1209 if let Some(directory) = directories.get_mut(replica_slot) {
1210 *directory = assignment.directory;
1211 }
1212
1213 Ok(KPartitionChangeRecord {
1214 partition_id: assignment.partition,
1215 topic_id: to_kuuid(topic.topic_id),
1216 directories: Some(directories.into_iter().map(to_kuuid).collect()),
1217 ..Default::default()
1218 })
1219}
1220
1221fn partition_state_to_kraft(
1222 image: &MetadataImage,
1223 topic: &str,
1224 partition: i32,
1225 leader_recovery_state: i8,
1226 eligible: Option<&[NodeId]>,
1227 last_known: Option<&[NodeId]>,
1228) -> Result<KPartitionChangeRecord, TranslateError> {
1229 let topic_id = image
1230 .topic(topic)
1231 .ok_or_else(|| TranslateError::UnknownTopicName(topic.to_owned()))?
1232 .topic_id;
1233 let nodes = |values: &[NodeId], field| {
1234 values
1235 .iter()
1236 .map(|node| {
1237 i32::try_from(node.0).map_err(|_| TranslateError::InvalidValue {
1238 field,
1239 detail: format!("node id {node} exceeds i32"),
1240 })
1241 })
1242 .collect::<Result<Vec<_>, _>>()
1243 };
1244 Ok(KPartitionChangeRecord {
1245 partition_id: partition,
1246 topic_id: to_kuuid(topic_id),
1247 leader_recovery_state,
1248 eligible_leader_replicas: eligible
1249 .map(|values| nodes(values, "eligible leader replica"))
1250 .transpose()?,
1251 last_known_elr: last_known
1252 .map(|values| nodes(values, "last known eligible leader replica"))
1253 .transpose()?,
1254 ..Default::default()
1255 })
1256}
1257
1258fn partition_update_to_kraft(
1259 update: &crate::PartitionUpdateRecord,
1260 image: &MetadataImage,
1261) -> Result<KPartitionChangeRecord, TranslateError> {
1262 let current = image
1263 .partition(&update.partition.topic, update.partition.partition)
1264 .ok_or_else(|| TranslateError::InvalidReference {
1265 field: "partition update",
1266 detail: format!(
1267 "unknown partition {}-{}",
1268 update.partition.topic, update.partition.partition
1269 ),
1270 })?;
1271 let mut change = partition_state_to_kraft(
1272 image,
1273 &update.partition.topic,
1274 update.partition.partition,
1275 update.recovery_state.map_or(-1, |state| state as i8),
1276 update.eligible_leader_replicas.as_deref(),
1277 update.last_known_elr.as_deref(),
1278 )?;
1279 let cast = |values: &[NodeId], field| {
1280 values
1281 .iter()
1282 .map(|node| {
1283 i32::try_from(node.0).map_err(|_| TranslateError::InvalidValue {
1284 field,
1285 detail: format!("node id {node} exceeds i32"),
1286 })
1287 })
1288 .collect::<Result<Vec<_>, _>>()
1289 };
1290 if update.partition.replicas != current.replicas {
1291 change.replicas = Some(cast(&update.partition.replicas, "partition replicas")?);
1292 }
1293 if update.partition.isr != current.isr {
1294 change.isr = Some(cast(&update.partition.isr, "partition isr")?);
1295 }
1296 if update.partition.removing_replicas != current.removing_replicas {
1297 change.removing_replicas = Some(cast(
1298 &update.partition.removing_replicas,
1299 "partition removing replicas",
1300 )?);
1301 }
1302 if update.partition.adding_replicas != current.adding_replicas {
1303 change.adding_replicas = Some(cast(
1304 &update.partition.adding_replicas,
1305 "partition adding replicas",
1306 )?);
1307 }
1308 if update.partition.leader != current.leader {
1309 change.leader = partition_leader_to_wire(update.partition.leader, "partition leader")?;
1310 }
1311 if update.partition.directories != current.directories {
1312 change.directories = Some(
1313 update
1314 .partition
1315 .directories
1316 .iter()
1317 .copied()
1318 .map(to_kuuid)
1319 .collect(),
1320 );
1321 }
1322 Ok(change)
1323}
1324
1325fn client_quota_to_kraft(q: &ClientQuotaRecord) -> KClientQuotaRecord {
1326 let entity = q
1327 .entity
1328 .iter()
1329 .map(|e| EntityData {
1330 entity_type: e.entity_type.clone(),
1331 entity_name: e.entity_name.clone(),
1332 ..Default::default()
1333 })
1334 .collect();
1335 let (value, remove) = q.config_value.map_or((0.0, true), |v| (v, false));
1338 KClientQuotaRecord {
1339 entity,
1340 key: q.config_key.clone(),
1341 value,
1342 remove,
1343 ..Default::default()
1344 }
1345}
1346
1347fn delegation_token_to_kraft(t: &DelegationTokenRecord) -> KDelegationTokenRecord {
1348 KDelegationTokenRecord {
1349 owner: t.owner.to_string(),
1350 requester: t.requester.to_string(),
1351 renewers: t.renewers.iter().map(ToString::to_string).collect(),
1352 issue_timestamp: t.issue_timestamp_ms,
1353 max_timestamp: t.max_timestamp_ms,
1354 expiration_timestamp: t.expiry_timestamp_ms,
1355 token_id: t.token_id.clone(),
1356 ..Default::default()
1357 }
1358}
1359
1360pub fn from_kraft(
1377 rec: &KraftMetadataRecord,
1378 image: &MetadataImage,
1379) -> Result<MetadataRecord, TranslateError> {
1380 match rec {
1381 KraftMetadataRecord::FeatureLevel(f) => {
1382 Ok(MetadataRecord::V1FeatureLevel(FeatureLevelRecord {
1383 name: f.name.clone(),
1384 level: f.feature_level,
1385 }))
1386 }
1387 KraftMetadataRecord::RegisterBroker(b) => Ok(MetadataRecord::V1BrokerRegistration(
1388 register_broker_from_kraft(b)?,
1389 )),
1390 KraftMetadataRecord::BrokerRegistrationChange(change) => {
1391 Ok(MetadataRecord::V1BrokerRegistrationChange(
1392 broker_registration_change_from_kraft(change)?,
1393 ))
1394 }
1395 KraftMetadataRecord::RegisterController(c) => Ok(MetadataRecord::V1ControllerRegistration(
1396 register_controller_from_kraft(c)?,
1397 )),
1398 KraftMetadataRecord::UnregisterController(u) => Ok(MetadataRecord::V1UnregisterController(
1399 UnregisterControllerRecord {
1400 node_id: node_id_from_wire(u.controller_id, "unregister controller id")?,
1401 },
1402 )),
1403 KraftMetadataRecord::UnregisterBroker(u) => {
1404 Ok(MetadataRecord::V1UnregisterBroker(UnregisterBrokerRecord {
1405 node_id: node_id_from_wire(u.broker_id, "unregister broker id")?,
1406 broker_epoch: u.broker_epoch,
1407 }))
1408 }
1409 KraftMetadataRecord::UserScramCredential(s) => {
1410 Ok(MetadataRecord::V1ScramCredential(ScramCredentialRecord {
1411 user: s.name.clone(),
1412 mechanism: scram_mechanism_from_wire(s.mechanism)?,
1413 salt: s.salt.to_vec(),
1414 stored_key: s.stored_key.to_vec(),
1415 server_key: s.server_key.to_vec(),
1416 iterations: u32::try_from(s.iterations).map_err(|_| {
1417 TranslateError::InvalidValue {
1418 field: "scram iterations",
1419 detail: format!("{} is negative", s.iterations),
1420 }
1421 })?,
1422 }))
1423 }
1424 KraftMetadataRecord::RemoveUserScramCredential(s) => Ok(
1425 MetadataRecord::V1DeleteScramCredential(DeleteScramCredentialRecord {
1426 user: s.name.clone(),
1427 mechanism: scram_mechanism_from_wire(s.mechanism)?,
1428 }),
1429 ),
1430 KraftMetadataRecord::AccessControlEntry(e) => {
1431 Ok(MetadataRecord::V1AccessControlEntry(AclEntry {
1432 resource_type: resource_type_from_wire(e.resource_type)?,
1433 resource_name: e.resource_name.clone(),
1434 pattern_type: pattern_type_from_wire(e.pattern_type)?,
1435 principal: e.principal.clone(),
1436 host: e.host.clone(),
1437 operation: operation_from_wire(e.operation)?,
1438 permission_type: permission_from_wire(e.permission_type)?,
1439 }))
1440 }
1441 KraftMetadataRecord::ClientQuota(q) => {
1442 Ok(MetadataRecord::V1ClientQuota(client_quota_from_kraft(q)))
1443 }
1444 KraftMetadataRecord::ProducerIds(record) => {
1445 Ok(MetadataRecord::V1ProducerIds(ProducerIdsRecord {
1446 broker_id: node_id_from_wire(record.broker_id, "producer IDs broker id")?,
1447 broker_epoch: record.broker_epoch,
1448 next_producer_id: record.next_producer_id,
1449 }))
1450 }
1451 KraftMetadataRecord::DelegationToken(t) => Ok(MetadataRecord::V1DelegationToken(
1452 delegation_token_from_kraft(t)?,
1453 )),
1454 KraftMetadataRecord::RemoveDelegationToken(t) => Ok(
1455 MetadataRecord::V1DeleteDelegationToken(DeleteDelegationTokenRecord {
1456 token_id: t.token_id.clone(),
1457 }),
1458 ),
1459 KraftMetadataRecord::Config(c) => config_from_kraft(c, image),
1460 KraftMetadataRecord::Topic(t) => Ok(MetadataRecord::V1Topic(topic_from_kraft(t, image))),
1461 KraftMetadataRecord::Partition(p) => partition_from_kraft(p, image),
1462 KraftMetadataRecord::PartitionChange(p) => partition_change_from_kraft(p, image),
1463 KraftMetadataRecord::RemoveTopic(t) => {
1464 let id = from_kuuid(t.topic_id);
1465 let name = topic_name_for_id(image, id).ok_or(TranslateError::UnknownTopicId(id))?;
1466 Ok(MetadataRecord::V1DeleteTopic(DeleteTopicRecord { name }))
1467 }
1468 KraftMetadataRecord::RemoveAccessControlEntry(r) => {
1469 let entry = image
1475 .all_acls()
1476 .find(|e| acl_id(e) == r.id)
1477 .ok_or_else(|| TranslateError::UnknownAclId(from_kuuid(r.id)))?;
1478 Ok(MetadataRecord::V1DeleteAccessControlEntry(exact_filter(
1479 entry,
1480 )))
1481 }
1482 KraftMetadataRecord::NoOp(no_op) => from_private_carrier(no_op),
1485 other => Err(TranslateError::NoCounterpart(kraft_variant_name(other))),
1486 }
1487}
1488
1489fn kraft_variant_name(rec: &KraftMetadataRecord) -> &'static str {
1490 match rec {
1491 KraftMetadataRecord::BrokerRegistrationChange(_) => "BrokerRegistrationChange",
1492 KraftMetadataRecord::PartitionChange(_) => "PartitionChange",
1493 KraftMetadataRecord::NoOp(_) => "NoOp",
1494 KraftMetadataRecord::BeginTransaction(_) => "BeginTransaction",
1495 KraftMetadataRecord::EndTransaction(_) => "EndTransaction",
1496 KraftMetadataRecord::RemoveAccessControlEntry(_) => "RemoveAccessControlEntry",
1497 KraftMetadataRecord::Unknown { .. } => "Unknown",
1498 _ => "unmodeled",
1499 }
1500}
1501
1502fn register_broker_from_kraft(
1503 b: &RegisterBrokerRecord,
1504) -> Result<BrokerRegistrationRecord, TranslateError> {
1505 let (host, port, endpoints_on_wire) = match b.end_points.split_first() {
1510 Some((head, rest)) if head.name.is_empty() => (head.host.clone(), head.port, rest),
1511 Some((head, _)) => (head.host.clone(), head.port, b.end_points.as_slice()),
1512 None => (String::new(), 0, &[][..]),
1513 };
1514 let endpoints = endpoints_on_wire
1515 .iter()
1516 .map(|e| {
1517 Ok(BrokerEndpoint {
1518 name: e.name.clone(),
1519 host: e.host.clone(),
1520 port: e.port,
1521 protocol: protocol_from_wire(e.security_protocol)?,
1522 })
1523 })
1524 .collect::<Result<Vec<_>, TranslateError>>()?;
1525 let features = b
1526 .features
1527 .iter()
1528 .map(|feature| {
1529 (
1530 feature.name.clone(),
1531 (feature.min_supported_version, feature.max_supported_version),
1532 )
1533 })
1534 .collect();
1535 let log_dirs = b.log_dirs.iter().copied().map(from_kuuid).collect();
1536 Ok(BrokerRegistrationRecord {
1537 node_id: node_id_from_wire(b.broker_id, "register broker id")?,
1538 broker_epoch: b.broker_epoch,
1539 incarnation_id: from_kuuid(b.incarnation_id),
1540 host,
1541 port,
1542 rack: b.rack.clone(),
1543 endpoints,
1544 log_dirs,
1545 fenced: b.fenced,
1546 in_controlled_shutdown: b.in_controlled_shutdown,
1547 cordoned_log_dirs: b
1548 .cordoned_log_dirs
1549 .as_ref()
1550 .map(|dirs| dirs.iter().copied().map(from_kuuid).collect()),
1551 features,
1552 })
1553}
1554
1555fn broker_registration_change_from_kraft(
1561 change: &BrokerRegistrationChangeRecord,
1562) -> Result<ChangeRecord, TranslateError> {
1563 let fenced =
1564 FencingChange::from_wire(change.fenced).ok_or_else(|| TranslateError::InvalidValue {
1565 field: "broker registration change fenced",
1566 detail: format!("unknown value {}", change.fenced),
1567 })?;
1568 let in_controlled_shutdown = match change.in_controlled_shutdown {
1569 0 => false,
1570 1 => true,
1571 other => {
1572 return Err(TranslateError::InvalidValue {
1573 field: "broker registration change in_controlled_shutdown",
1574 detail: format!("unknown value {other}"),
1575 });
1576 }
1577 };
1578 Ok(ChangeRecord {
1579 node_id: node_id_from_wire(change.broker_id, "broker registration change id")?,
1580 broker_epoch: change.broker_epoch,
1581 fenced,
1582 in_controlled_shutdown,
1583 log_dirs: change.log_dirs.iter().copied().map(from_kuuid).collect(),
1584 cordoned_log_dirs: change
1585 .cordoned_log_dirs
1586 .as_ref()
1587 .map(|dirs| dirs.iter().copied().map(from_kuuid).collect()),
1588 })
1589}
1590
1591fn broker_registration_change_to_kraft(
1595 change: &ChangeRecord,
1596) -> Result<BrokerRegistrationChangeRecord, TranslateError> {
1597 Ok(BrokerRegistrationChangeRecord {
1598 broker_id: node_id_to_wire(change.node_id, "broker_id")?,
1599 broker_epoch: change.broker_epoch,
1600 fenced: change.fenced.wire_value(),
1601 in_controlled_shutdown: i8::from(change.in_controlled_shutdown),
1602 log_dirs: change.log_dirs.iter().copied().map(to_kuuid).collect(),
1603 cordoned_log_dirs: change
1604 .cordoned_log_dirs
1605 .as_ref()
1606 .map(|dirs| dirs.iter().copied().map(to_kuuid).collect()),
1607 ..Default::default()
1608 })
1609}
1610
1611fn broker_registration_change_version(change: &BrokerRegistrationChangeRecord) -> i16 {
1616 if change.cordoned_log_dirs.is_some() {
1617 3
1618 } else if !change.log_dirs.is_empty() {
1619 2
1620 } else {
1621 i16::from(change.in_controlled_shutdown != 0)
1622 }
1623}
1624
1625fn register_controller_from_kraft(
1626 controller: &RegisterControllerRecord,
1627) -> Result<ControllerRegistrationRecord, TranslateError> {
1628 let endpoints = controller
1629 .end_points
1630 .iter()
1631 .map(|endpoint| {
1632 Ok(BrokerEndpoint {
1633 name: endpoint.name.clone(),
1634 host: endpoint.host.clone(),
1635 port: endpoint.port,
1636 protocol: protocol_from_wire(endpoint.security_protocol)?,
1637 })
1638 })
1639 .collect::<Result<Vec<_>, TranslateError>>()?;
1640 let features = controller
1641 .features
1642 .iter()
1643 .map(|feature| {
1644 (
1645 feature.name.clone(),
1646 (feature.min_supported_version, feature.max_supported_version),
1647 )
1648 })
1649 .collect();
1650 Ok(ControllerRegistrationRecord {
1651 node_id: node_id_from_wire(controller.controller_id, "controller id")?,
1652 incarnation_id: from_kuuid(controller.incarnation_id),
1653 zk_migration_ready: controller.zk_migration_ready,
1654 endpoints,
1655 features,
1656 })
1657}
1658
1659fn topic_from_kraft(t: &KTopicRecord, image: &MetadataImage) -> TopicRecord {
1660 let name = t.name.clone();
1661 let partitions = image.topic_partition_count(&name);
1666 let replication_factor = image
1667 .partitions_of(&name)
1668 .next()
1669 .map_or(1, |p| i16::try_from(p.replicas.len()).unwrap_or(1));
1670 TopicRecord {
1671 name,
1672 topic_id: from_kuuid(t.topic_id),
1673 partitions,
1674 replication_factor,
1675 }
1676}
1677
1678fn partition_from_kraft(
1679 p: &KPartitionRecord,
1680 image: &MetadataImage,
1681) -> Result<MetadataRecord, TranslateError> {
1682 let id = from_kuuid(p.topic_id);
1683 let topic = topic_name_for_id(image, id).ok_or(TranslateError::UnknownTopicId(id))?;
1684 let nodes = |values: &[i32], field| {
1685 values
1686 .iter()
1687 .map(|value| node_id_from_wire(*value, field))
1688 .collect::<Result<Vec<_>, _>>()
1689 };
1690 let partition = PartitionRecord {
1691 topic,
1692 partition: p.partition_id,
1693 leader: partition_leader_from_wire(p.leader, "partition leader")?,
1694 replicas: nodes(&p.replicas, "partition replica")?,
1695 isr: nodes(&p.isr, "partition ISR member")?,
1696 leader_epoch: LeaderEpoch(p.leader_epoch),
1698 partition_epoch: p.partition_epoch,
1699 adding_replicas: nodes(&p.adding_replicas, "adding replica")?,
1700 removing_replicas: nodes(&p.removing_replicas, "removing replica")?,
1701 directories: p.directories.iter().map(|u| from_kuuid(*u)).collect(),
1703 };
1704 let recovery_state = match p.leader_recovery_state {
1705 0 => LeaderRecoveryState::Recovered,
1706 1 => LeaderRecoveryState::Recovering,
1707 value => {
1708 return Err(TranslateError::InvalidValue {
1709 field: "partition leader recovery state",
1710 detail: format!("unknown state {value}"),
1711 });
1712 }
1713 };
1714 if p.eligible_leader_replicas.is_some()
1715 || p.last_known_elr.is_some()
1716 || recovery_state == LeaderRecoveryState::Recovering
1717 {
1718 Ok(MetadataRecord::V1PartitionUpdate(
1719 crate::PartitionUpdateRecord {
1720 partition,
1721 eligible_leader_replicas: p
1722 .eligible_leader_replicas
1723 .as_deref()
1724 .map(|values| nodes(values, "eligible leader replica"))
1725 .transpose()?,
1726 last_known_elr: p
1727 .last_known_elr
1728 .as_deref()
1729 .map(|values| nodes(values, "last known eligible leader replica"))
1730 .transpose()?,
1731 recovery_state: Some(recovery_state),
1732 },
1733 ))
1734 } else {
1735 Ok(MetadataRecord::V1Partition(partition))
1736 }
1737}
1738
1739fn partition_change_from_kraft(
1740 change: &KPartitionChangeRecord,
1741 image: &MetadataImage,
1742) -> Result<MetadataRecord, TranslateError> {
1743 let topic_id = from_kuuid(change.topic_id);
1744 let topic =
1745 topic_name_for_id(image, topic_id).ok_or(TranslateError::UnknownTopicId(topic_id))?;
1746 let has_recovery = change.leader_recovery_state != -1;
1747 let has_elr = change.eligible_leader_replicas.is_some() || change.last_known_elr.is_some();
1748 let nodes = |values: &[i32], field| {
1749 values
1750 .iter()
1751 .map(|value| node_id_from_wire(*value, field))
1752 .collect::<Result<Vec<_>, _>>()
1753 };
1754 let recovery_state = if has_recovery {
1755 Some(match change.leader_recovery_state {
1756 0 => LeaderRecoveryState::Recovered,
1757 1 => LeaderRecoveryState::Recovering,
1758 value => {
1759 return Err(TranslateError::InvalidValue {
1760 field: "partition change leader recovery state",
1761 detail: format!("unknown state {value}"),
1762 });
1763 }
1764 })
1765 } else {
1766 None
1767 };
1768 let mut partition = image
1769 .partition(&topic, change.partition_id)
1770 .cloned()
1771 .ok_or_else(|| TranslateError::InvalidReference {
1772 field: "partition change",
1773 detail: format!("unknown partition {topic}-{}", change.partition_id),
1774 })?;
1775 if let Some(replicas) = &change.replicas {
1776 partition.replicas = nodes(replicas, "partition change replica")?;
1777 }
1778 if let Some(isr) = &change.isr {
1779 partition.isr = nodes(isr, "partition change ISR member")?;
1780 }
1781 if let Some(removing) = &change.removing_replicas {
1782 partition.removing_replicas = nodes(removing, "partition change removing replica")?;
1783 }
1784 if let Some(adding) = &change.adding_replicas {
1785 partition.adding_replicas = nodes(adding, "partition change adding replica")?;
1786 }
1787 if change.leader != -2 {
1788 partition.leader = partition_leader_from_wire(change.leader, "partition change leader")?;
1789 partition.leader_epoch =
1792 LeaderEpoch(partition.leader_epoch.get().checked_add(1).ok_or_else(|| {
1793 TranslateError::InvalidReference {
1794 field: "partition change leader epoch",
1795 detail: "leader epoch overflow".into(),
1796 }
1797 })?);
1798 }
1799 if let Some(directories) = &change.directories {
1800 if directories.len() != partition.replicas.len() {
1801 let field = "partition change directories";
1802 let detail = format!(
1803 "{} directories for {} replicas",
1804 directories.len(),
1805 partition.replicas.len()
1806 );
1807 return Err(if change.replicas.is_some() {
1812 TranslateError::InvalidValue { field, detail }
1813 } else {
1814 TranslateError::InvalidReference { field, detail }
1815 });
1816 }
1817 partition.directories = directories.iter().copied().map(from_kuuid).collect();
1818 }
1819 partition.partition_epoch = partition.partition_epoch.checked_add(1).ok_or_else(|| {
1822 TranslateError::InvalidReference {
1823 field: "partition change partition epoch",
1824 detail: "partition epoch overflow".into(),
1825 }
1826 })?;
1827 if has_recovery || has_elr {
1828 Ok(MetadataRecord::V1PartitionUpdate(
1829 crate::PartitionUpdateRecord {
1830 partition,
1831 eligible_leader_replicas: change
1832 .eligible_leader_replicas
1833 .as_deref()
1834 .map(|values| nodes(values, "eligible leader replica"))
1835 .transpose()?,
1836 last_known_elr: change
1837 .last_known_elr
1838 .as_deref()
1839 .map(|values| nodes(values, "last known eligible leader replica"))
1840 .transpose()?,
1841 recovery_state,
1842 },
1843 ))
1844 } else {
1845 Ok(MetadataRecord::V1Partition(partition))
1846 }
1847}
1848
1849fn partition_leader_to_wire(leader: NodeId, field: &'static str) -> Result<i32, TranslateError> {
1850 if leader == NodeId(0) {
1851 return Ok(-1);
1852 }
1853 i32::try_from(leader.0).map_err(|_| TranslateError::InvalidValue {
1854 field,
1855 detail: format!("leader {leader} exceeds i32"),
1856 })
1857}
1858
1859fn partition_leader_from_wire(value: i32, field: &'static str) -> Result<NodeId, TranslateError> {
1860 if value == -1 {
1861 return Ok(NodeId(0));
1862 }
1863 node_id_from_wire(value, field)
1864}
1865
1866fn node_id_from_wire(value: i32, field: &'static str) -> Result<NodeId, TranslateError> {
1867 u64::try_from(value)
1868 .map(NodeId)
1869 .map_err(|_| TranslateError::InvalidValue {
1870 field,
1871 detail: format!("{value} is negative"),
1872 })
1873}
1874
1875fn node_id_to_wire(node_id: NodeId, field: &'static str) -> Result<i32, TranslateError> {
1876 i32::try_from(node_id.0).map_err(|_| TranslateError::InvalidValue {
1877 field,
1878 detail: format!("node_id {node_id} exceeds i32"),
1879 })
1880}
1881
1882fn client_quota_from_kraft(q: &KClientQuotaRecord) -> ClientQuotaRecord {
1883 let entity = q
1884 .entity
1885 .iter()
1886 .map(|e| QuotaEntity {
1887 entity_type: e.entity_type.clone(),
1888 entity_name: e.entity_name.clone(),
1889 })
1890 .collect();
1891 let config_value = if q.remove { None } else { Some(q.value) };
1892 ClientQuotaRecord {
1893 entity,
1894 config_key: q.key.clone(),
1895 config_value,
1896 }
1897}
1898
1899fn delegation_token_from_kraft(
1900 t: &KDelegationTokenRecord,
1901) -> Result<DelegationTokenRecord, TranslateError> {
1902 let owner = t
1903 .owner
1904 .parse::<KafkaPrincipal>()
1905 .map_err(|e| TranslateError::InvalidValue {
1906 field: "delegation token owner",
1907 detail: e,
1908 })?;
1909 let requester =
1910 t.requester
1911 .parse::<KafkaPrincipal>()
1912 .map_err(|e| TranslateError::InvalidValue {
1913 field: "delegation token requester",
1914 detail: e,
1915 })?;
1916 let renewers = t
1917 .renewers
1918 .iter()
1919 .map(|r| {
1920 r.parse::<KafkaPrincipal>()
1921 .map_err(|e| TranslateError::InvalidValue {
1922 field: "delegation token renewer",
1923 detail: e,
1924 })
1925 })
1926 .collect::<Result<Vec<_>, _>>()?;
1927 Ok(DelegationTokenRecord {
1928 token_id: t.token_id.clone(),
1929 owner,
1930 requester,
1931 issue_timestamp_ms: t.issue_timestamp,
1932 expiry_timestamp_ms: t.expiration_timestamp,
1933 max_timestamp_ms: t.max_timestamp,
1934 renewers,
1935 })
1936}
1937
1938fn config_from_kraft(
1939 c: &ConfigRecord,
1940 image: &MetadataImage,
1941) -> Result<MetadataRecord, TranslateError> {
1942 match c.resource_type {
1943 4 => {
1944 let node_id = if c.resource_name.is_empty() {
1947 DEFAULT_BROKER_CONFIG_NODE_ID
1948 } else {
1949 NodeId(c.resource_name.parse::<u64>().map_err(|e| {
1950 TranslateError::InvalidValue {
1951 field: "broker config resource_name",
1952 detail: e.to_string(),
1953 }
1954 })?)
1955 };
1956 Ok(MetadataRecord::V1BrokerConfig(BrokerConfigRecord {
1957 node_id,
1958 config_name: c.name.clone(),
1959 config_value: c.value.clone(),
1960 }))
1961 }
1962 2 => {
1963 let mut overrides = image
1971 .topic_config(&c.resource_name)
1972 .cloned()
1973 .unwrap_or_default();
1974 match &c.value {
1975 Some(v) => {
1976 overrides.insert(c.name.clone(), v.clone());
1977 }
1978 None => {
1979 overrides.remove(&c.name);
1980 }
1981 }
1982 Ok(MetadataRecord::V1TopicConfig(TopicConfigRecord {
1983 topic: c.resource_name.clone(),
1984 overrides,
1985 }))
1986 }
1987 16 => {
1988 let mut configs = image
1989 .client_metrics_config(&c.resource_name)
1990 .cloned()
1991 .unwrap_or_default();
1992 match &c.value {
1993 Some(v) => {
1994 configs.insert(c.name.clone(), v.clone());
1995 }
1996 None => {
1997 configs.remove(&c.name);
1998 }
1999 }
2000 Ok(MetadataRecord::V1ClientMetricsConfig(
2001 ClientMetricsConfigRecord {
2002 name: c.resource_name.clone(),
2003 configs,
2004 },
2005 ))
2006 }
2007 32 => {
2008 let mut configs = image
2009 .group_config(&c.resource_name)
2010 .cloned()
2011 .unwrap_or_default();
2012 match &c.value {
2013 Some(v) => {
2014 configs.insert(c.name.clone(), v.clone());
2015 }
2016 None => {
2017 configs.remove(&c.name);
2018 }
2019 }
2020 Ok(MetadataRecord::V1GroupConfig(GroupConfigRecord {
2021 group_id: c.resource_name.clone(),
2022 configs,
2023 }))
2024 }
2025 other => Err(TranslateError::InvalidValue {
2026 field: "config resource_type",
2027 detail: format!("unsupported resource_type {other}"),
2028 }),
2029 }
2030}
2031
2032fn topic_name_for_id(image: &MetadataImage, id: uuid::Uuid) -> Option<String> {
2034 image
2035 .topics()
2036 .find(|t| t.topic_id == id)
2037 .map(|t| t.name.clone())
2038}
2039
2040#[cfg(test)]
2041mod tests {
2042 use assert2::check;
2043 use krabka_protocol::owned::{
2044 begin_transaction_record::BeginTransactionRecord,
2045 end_transaction_record::EndTransactionRecord,
2046 };
2047
2048 use super::*;
2049 use crate::{
2050 acl::AclEntryFilter,
2051 break_glass::{BreakGlassAction, BreakGlassApproval, BreakGlassProposalRecord},
2052 metadata_version::METADATA_VERSION_FEATURE,
2053 records::{
2054 FeaturesEpochRecord, KRaftVersionRecord, PartitionDirAssignmentRecord, VotersRecord,
2055 },
2056 voters::VoterSet,
2057 write_freeze::TopicFreezeRecord,
2058 };
2059
2060 fn img() -> MetadataImage {
2061 MetadataImage::new(uuid::Uuid::nil())
2062 }
2063
2064 fn round_trip(rec: &MetadataRecord, image: &MetadataImage) {
2065 let k = to_kraft(rec, image).unwrap();
2066 assert2::assert!(from_kraft(&k, image).unwrap() == *rec);
2067 }
2068
2069 #[test]
2072 fn feature_level_round_trips() {
2073 let rec = MetadataRecord::V1FeatureLevel(FeatureLevelRecord {
2074 name: "metadata.version".into(),
2075 level: 25,
2076 });
2077 round_trip(&rec, &img());
2078 }
2079
2080 #[test]
2081 fn register_broker_no_endpoints_round_trips() {
2082 round_trip(
2085 &MetadataRecord::V1BrokerRegistration(BrokerRegistrationRecord {
2086 node_id: NodeId(7),
2087 broker_epoch: 42,
2088 incarnation_id: uuid::Uuid::from_u128(0xdeadbeef_cafe_babe_0123_456789abcdef),
2089 host: "192.168.1.10".into(),
2090 port: 9092,
2091 rack: Some("us-east-1a".into()),
2092 log_dirs: vec![],
2093 fenced: false,
2094 in_controlled_shutdown: false,
2095 cordoned_log_dirs: None,
2096 endpoints: vec![],
2097 features: BTreeMap::new(),
2098 }),
2099 &img(),
2100 );
2101 }
2102
2103 #[test]
2104 fn register_broker_with_endpoints_round_trips() {
2105 round_trip(
2108 &MetadataRecord::V1BrokerRegistration(BrokerRegistrationRecord {
2109 node_id: NodeId(1),
2110 broker_epoch: 7,
2111 incarnation_id: uuid::Uuid::from_u128(0xfeedface_0000_0000_0000_000000000001),
2112 host: "ext.example.com".into(),
2113 port: 9093,
2114 rack: None,
2115 log_dirs: vec![],
2116 fenced: false,
2117 in_controlled_shutdown: false,
2118 cordoned_log_dirs: None,
2119 endpoints: vec![
2120 BrokerEndpoint {
2121 name: "EXTERNAL".into(),
2122 host: "ext.example.com".into(),
2123 port: 9093,
2124 protocol: ListenerProtocol::SaslSsl,
2125 },
2126 BrokerEndpoint {
2127 name: "INTERNAL".into(),
2128 host: "int".into(),
2129 port: 9094,
2130 protocol: ListenerProtocol::Plaintext,
2131 },
2132 ],
2133 features: BTreeMap::new(),
2134 }),
2135 &img(),
2136 );
2137 }
2138
2139 #[test]
2140 fn register_broker_to_kraft_preserves_endpoint_details_and_unfenced_state() {
2141 let rec = MetadataRecord::V1BrokerRegistration(BrokerRegistrationRecord {
2142 node_id: NodeId(1),
2143 broker_epoch: 7,
2144 incarnation_id: uuid::Uuid::from_u128(0xfeedface_0000_0000_0000_000000000001),
2145 host: "broker.local".into(),
2146 port: 9092,
2147 rack: None,
2148 log_dirs: vec![],
2149 fenced: false,
2150 in_controlled_shutdown: false,
2151 cordoned_log_dirs: None,
2152 endpoints: vec![BrokerEndpoint {
2153 name: "EXTERNAL".into(),
2154 host: "ext.example.com".into(),
2155 port: 9093,
2156 protocol: ListenerProtocol::SaslSsl,
2157 }],
2158 features: BTreeMap::new(),
2159 });
2160
2161 let KraftMetadataRecord::RegisterBroker(k) = to_kraft(&rec, &img()).unwrap() else {
2162 panic!("expected RegisterBroker");
2163 };
2164
2165 check!(
2166 (
2167 k.fenced,
2168 k.end_points
2169 .iter()
2170 .map(|endpoint| (endpoint.name.as_str(), endpoint.security_protocol))
2171 .collect::<Vec<_>>(),
2172 ) == (
2173 false,
2174 vec![("EXTERNAL", protocol_to_wire(ListenerProtocol::SaslSsl),)],
2175 )
2176 );
2177 }
2178
2179 #[test]
2180 fn controller_registration_round_trips() {
2181 round_trip(
2182 &MetadataRecord::V1ControllerRegistration(ControllerRegistrationRecord {
2183 node_id: NodeId(3),
2184 incarnation_id: uuid::Uuid::from_u128(3),
2185 zk_migration_ready: false,
2186 endpoints: vec![BrokerEndpoint {
2187 name: "CONTROLLER".into(),
2188 host: "controller-3".into(),
2189 port: 9093,
2190 protocol: ListenerProtocol::Plaintext,
2191 }],
2192 features: BTreeMap::from([("metadata.version".into(), (7, 25))]),
2193 }),
2194 &img(),
2195 );
2196 }
2197
2198 #[test]
2199 fn register_broker_incarnation_id_survives_round_trip() {
2200 let id = uuid::Uuid::from_u128(0x0102_0304_0506_0708_090a_0b0c_0d0e_0f10);
2201 round_trip(
2202 &MetadataRecord::V1BrokerRegistration(BrokerRegistrationRecord {
2203 node_id: NodeId(3),
2204 broker_epoch: 5,
2205 incarnation_id: id,
2206 host: "127.0.0.1".into(),
2207 port: 9092,
2208 rack: None,
2209 log_dirs: vec![],
2210 fenced: false,
2211 in_controlled_shutdown: false,
2212 cordoned_log_dirs: None,
2213 endpoints: vec![],
2214 features: BTreeMap::new(),
2215 }),
2216 &img(),
2217 );
2218 }
2219
2220 #[test]
2221 fn register_broker_log_dirs_follow_metadata_version_record_gate() {
2222 let directory = uuid::Uuid::from_u128(0xD1);
2223 let record = MetadataRecord::V1BrokerRegistration(BrokerRegistrationRecord {
2224 node_id: NodeId(3),
2225 broker_epoch: 5,
2226 incarnation_id: uuid::Uuid::from_u128(3),
2227 host: "127.0.0.1".into(),
2228 port: 9092,
2229 rack: None,
2230 endpoints: vec![],
2231 log_dirs: vec![directory],
2232 fenced: false,
2233 in_controlled_shutdown: false,
2234 cordoned_log_dirs: None,
2235 features: BTreeMap::new(),
2236 });
2237
2238 let mut kip858_image = img();
2239 kip858_image.apply(&MetadataRecord::V1FeatureLevel(FeatureLevelRecord {
2240 name: METADATA_VERSION_FEATURE.into(),
2241 level: DIRECTORY_ASSIGNMENT_MIN_LEVEL,
2242 }));
2243 let value = to_kraft_values(&record, &kip858_image).unwrap().remove(0);
2244 let (wire, version) = KraftMetadataRecord::decode_value(&value).unwrap();
2245 assert2::assert!(version == 3);
2246 assert2::assert!(from_kraft(&wire, &kip858_image).unwrap() == record);
2247
2248 let mut pre_kip858_image = img();
2249 pre_kip858_image.apply(&MetadataRecord::V1FeatureLevel(FeatureLevelRecord {
2250 name: METADATA_VERSION_FEATURE.into(),
2251 level: DIRECTORY_ASSIGNMENT_MIN_LEVEL - 1,
2252 }));
2253 let value = to_kraft_values(&record, &pre_kip858_image)
2254 .unwrap()
2255 .remove(0);
2256 let (wire, version) = KraftMetadataRecord::decode_value(&value).unwrap();
2257 assert2::assert!(version == 2);
2258 let MetadataRecord::V1BrokerRegistration(decoded) =
2259 from_kraft(&wire, &pre_kip858_image).unwrap()
2260 else {
2261 panic!("expected broker registration")
2262 };
2263 assert2::assert!(decoded.log_dirs.is_empty());
2264 }
2265
2266 #[test]
2267 fn partition_epoch_survives_round_trip() {
2268 let mut image = img();
2269 let tid = uuid::Uuid::from_u128(99);
2270 image.apply(&MetadataRecord::V1Topic(TopicRecord {
2271 name: "epoch-test".into(),
2272 topic_id: tid,
2273 partitions: 1,
2274 replication_factor: 1,
2275 }));
2276 let rec = MetadataRecord::V1Partition(PartitionRecord {
2277 topic: "epoch-test".into(),
2278 partition: 0,
2279 leader: NodeId(1),
2280 replicas: vec![NodeId(1)],
2281 isr: vec![NodeId(1)],
2282 leader_epoch: LeaderEpoch(7),
2283 adding_replicas: vec![],
2284 removing_replicas: vec![],
2285 directories: vec![],
2286 partition_epoch: 42,
2287 });
2288 image.apply(&rec);
2289 round_trip(&rec, &image);
2290 }
2291
2292 #[test]
2297 fn register_broker_frames_at_kafkas_record_version() {
2298 use crate::metadata_version::{
2299 CORDONED_LOG_DIRS_MIN_LEVEL, DIRECTORY_ASSIGNMENT_MIN_LEVEL, METADATA_VERSION_MAX,
2300 METADATA_VERSION_MIN,
2301 };
2302
2303 let directory = uuid::Uuid::from_u128(0xD1);
2306 let full = BrokerRegistrationRecord {
2307 node_id: NodeId(3),
2308 broker_epoch: 5,
2309 incarnation_id: uuid::Uuid::from_u128(3),
2310 host: "127.0.0.1".into(),
2311 port: 9092,
2312 rack: None,
2313 endpoints: vec![],
2314 log_dirs: vec![directory],
2315 fenced: true,
2316 in_controlled_shutdown: true,
2317 cordoned_log_dirs: Some(vec![directory]),
2318 features: BTreeMap::new(),
2319 };
2320 let without_cordoned = BrokerRegistrationRecord {
2321 cordoned_log_dirs: None,
2322 ..full.clone()
2323 };
2324 let without_dirs = BrokerRegistrationRecord {
2325 log_dirs: vec![],
2326 ..without_cordoned.clone()
2327 };
2328
2329 for (level, want_version, want) in [
2330 (METADATA_VERSION_MAX, 4_i16, &full),
2331 (CORDONED_LOG_DIRS_MIN_LEVEL, 4, &full),
2332 (CORDONED_LOG_DIRS_MIN_LEVEL - 1, 3, &without_cordoned),
2333 (DIRECTORY_ASSIGNMENT_MIN_LEVEL, 3, &without_cordoned),
2334 (DIRECTORY_ASSIGNMENT_MIN_LEVEL - 1, 2, &without_dirs),
2335 (8, 2, &without_dirs),
2336 (METADATA_VERSION_MIN, 1, &without_dirs),
2337 ] {
2338 let mut image = img();
2339 image.apply(&MetadataRecord::V1FeatureLevel(FeatureLevelRecord {
2340 name: METADATA_VERSION_FEATURE.into(),
2341 level,
2342 }));
2343 let record = MetadataRecord::V1BrokerRegistration(full.clone());
2344 let value = to_kraft_values(&record, &image).unwrap().remove(0);
2345 let (wire, version) = KraftMetadataRecord::decode_value(&value).unwrap();
2346 check!(
2347 (version, from_kraft(&wire, &image).unwrap())
2348 == (
2349 want_version,
2350 MetadataRecord::V1BrokerRegistration(want.clone())
2351 ),
2352 "metadata.version {level}"
2353 );
2354 }
2355 }
2356
2357 #[test]
2362 fn broker_registration_change_decodes_to_the_change_record() {
2363 let (d1, d2) = (uuid::Uuid::from_u128(0xD1), uuid::Uuid::from_u128(0xD2));
2364 let wire = |fenced: i8,
2365 in_controlled_shutdown: i8,
2366 log_dirs: Vec<uuid::Uuid>,
2367 cordoned: Option<Vec<uuid::Uuid>>| {
2368 BrokerRegistrationChangeRecord {
2369 broker_id: 3,
2370 broker_epoch: 5,
2371 fenced,
2372 in_controlled_shutdown,
2373 log_dirs: log_dirs.into_iter().map(to_kuuid).collect(),
2374 cordoned_log_dirs: cordoned.map(|dirs| dirs.into_iter().map(to_kuuid).collect()),
2375 ..Default::default()
2376 }
2377 };
2378 let decoded = |edit: fn(&mut ChangeRecord)| {
2379 let mut change = ChangeRecord::no_change(NodeId(3), 5);
2380 edit(&mut change);
2381 Ok(MetadataRecord::V1BrokerRegistrationChange(change))
2382 };
2383 let invalid = |field, detail: &str| {
2384 Err(TranslateError::InvalidValue {
2385 field,
2386 detail: detail.into(),
2387 })
2388 };
2389
2390 for (case, record, want) in [
2391 ("no change", wire(0, 0, vec![], None), decoded(|_| {})),
2392 (
2393 "unfence",
2394 wire(-1, 0, vec![], None),
2395 decoded(|c| c.fenced = FencingChange::Unfence),
2396 ),
2397 (
2398 "fence",
2399 wire(1, 0, vec![], None),
2400 decoded(|c| c.fenced = FencingChange::Fence),
2401 ),
2402 (
2403 "controlled shutdown",
2404 wire(0, 1, vec![], None),
2405 decoded(|c| c.in_controlled_shutdown = true),
2406 ),
2407 (
2408 "new log dirs",
2409 wire(0, 0, vec![d1, d2], None),
2410 decoded(|c| {
2411 c.log_dirs = vec![uuid::Uuid::from_u128(0xD1), uuid::Uuid::from_u128(0xD2)];
2412 }),
2413 ),
2414 (
2415 "uncordon every dir",
2416 wire(0, 0, vec![], Some(vec![])),
2417 decoded(|c| c.cordoned_log_dirs = Some(vec![])),
2418 ),
2419 (
2420 "unknown fenced value",
2421 wire(2, 0, vec![], None),
2422 invalid("broker registration change fenced", "unknown value 2"),
2423 ),
2424 (
2425 "unknown controlled shutdown value",
2426 wire(0, -1, vec![], None),
2427 invalid(
2428 "broker registration change in_controlled_shutdown",
2429 "unknown value -1",
2430 ),
2431 ),
2432 (
2433 "negative broker id",
2434 BrokerRegistrationChangeRecord {
2435 broker_id: -1,
2436 ..wire(0, 0, vec![], None)
2437 },
2438 invalid("broker registration change id", "-1 is negative"),
2439 ),
2440 ] {
2441 let value = KraftMetadataRecord::BrokerRegistrationChange(record)
2443 .encode_value(3)
2444 .unwrap();
2445 check!(from_kraft_value(&value, &img()) == want, "case {case}");
2446 }
2447 }
2448
2449 #[test]
2453 fn broker_registration_change_encodes_at_kafkas_record_version() {
2454 let dir = uuid::Uuid::from_u128(0xD1);
2455 let change = |edit: fn(&mut ChangeRecord)| {
2456 let mut change = ChangeRecord::no_change(NodeId(3), 5);
2457 edit(&mut change);
2458 change
2459 };
2460 let frame = |version: u8, tagged: &[u8]| {
2463 let mut bytes = vec![0x01, 0x11, version, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 5];
2464 bytes.extend_from_slice(tagged);
2465 bytes
2466 };
2467 let dir_bytes = dir.into_bytes();
2470 let mut log_dirs_tagged = vec![0x01, 0x02, 0x11, 0x02];
2471 log_dirs_tagged.extend_from_slice(&dir_bytes);
2472 let mut cordoned_tagged = vec![0x01, 0x03, 0x11, 0x02];
2473 cordoned_tagged.extend_from_slice(&dir_bytes);
2474
2475 for (case, record, wire) in [
2476 (
2477 "fence at v0",
2478 change(|c| c.fenced = FencingChange::Fence),
2479 frame(0, &[0x01, 0x00, 0x01, 0x01]),
2480 ),
2481 (
2482 "unfence at v0",
2483 change(|c| c.fenced = FencingChange::Unfence),
2484 frame(0, &[0x01, 0x00, 0x01, 0xff]),
2485 ),
2486 (
2487 "controlled shutdown at v1",
2488 change(|c| c.in_controlled_shutdown = true),
2489 frame(1, &[0x01, 0x01, 0x01, 0x01]),
2490 ),
2491 (
2492 "log dirs at v2",
2493 change(|c| c.log_dirs = vec![uuid::Uuid::from_u128(0xD1)]),
2494 frame(2, &log_dirs_tagged),
2495 ),
2496 (
2497 "cordoned log dirs at v3",
2498 change(|c| c.cordoned_log_dirs = Some(vec![uuid::Uuid::from_u128(0xD1)])),
2499 frame(3, &cordoned_tagged),
2500 ),
2501 ] {
2502 let record = MetadataRecord::V1BrokerRegistrationChange(record);
2503 let values = to_kraft_values(&record, &img()).unwrap();
2504 check!(
2505 values.iter().map(|v| v.to_vec()).collect::<Vec<_>>() == vec![wire],
2506 "{case}"
2507 );
2508 check!(from_kraft_value(&values[0], &img()) == Ok(record), "{case}");
2509 }
2510 }
2511
2512 #[test]
2516 fn broker_registration_change_drops_fields_the_metadata_version_lacks() {
2517 use crate::metadata_version::{
2518 CORDONED_LOG_DIRS_MIN_LEVEL, DIRECTORY_ASSIGNMENT_MIN_LEVEL,
2519 };
2520
2521 let dir = uuid::Uuid::from_u128(0xD1);
2522 let full = ChangeRecord {
2523 fenced: FencingChange::Unfence,
2524 log_dirs: vec![dir],
2525 cordoned_log_dirs: Some(vec![dir]),
2526 ..ChangeRecord::no_change(NodeId(3), 5)
2527 };
2528 for (level, want_version, want) in [
2529 (CORDONED_LOG_DIRS_MIN_LEVEL, 3, full.clone()),
2530 (
2531 CORDONED_LOG_DIRS_MIN_LEVEL - 1,
2532 2,
2533 ChangeRecord {
2534 cordoned_log_dirs: None,
2535 ..full.clone()
2536 },
2537 ),
2538 (
2539 DIRECTORY_ASSIGNMENT_MIN_LEVEL - 1,
2540 0,
2541 ChangeRecord {
2542 log_dirs: vec![],
2543 cordoned_log_dirs: None,
2544 ..full.clone()
2545 },
2546 ),
2547 ] {
2548 let mut image = img();
2549 image.apply(&MetadataRecord::V1FeatureLevel(FeatureLevelRecord {
2550 name: METADATA_VERSION_FEATURE.into(),
2551 level,
2552 }));
2553 let values = to_kraft_values(
2554 &MetadataRecord::V1BrokerRegistrationChange(full.clone()),
2555 &image,
2556 )
2557 .unwrap();
2558 let (_, version) = KraftMetadataRecord::decode_value(&values[0]).unwrap();
2559 check!(
2560 (version, from_kraft_value(&values[0], &image))
2561 == (
2562 want_version,
2563 Ok(MetadataRecord::V1BrokerRegistrationChange(want))
2564 ),
2565 "metadata.version {level}"
2566 );
2567 }
2568 }
2569
2570 #[test]
2573 fn unregister_controller_round_trips_as_api_key_29() {
2574 let record = MetadataRecord::V1UnregisterController(UnregisterControllerRecord {
2575 node_id: NodeId(3000),
2576 });
2577 let values = to_kraft_values(&record, &img()).unwrap();
2578 check!(
2579 values.iter().map(|v| v.to_vec()).collect::<Vec<_>>()
2580 == vec![vec![0x01, 0x1d, 0x00, 0x00, 0x00, 0x0b, 0xb8, 0x00]]
2581 );
2582 check!(from_kraft_value(&values[0], &img()) == Ok(record));
2583
2584 let negative = KraftMetadataRecord::UnregisterController(KUnregisterControllerRecord {
2585 controller_id: -1,
2586 ..Default::default()
2587 });
2588 check!(
2589 from_kraft(&negative, &img())
2590 == Err(TranslateError::InvalidValue {
2591 field: "unregister controller id",
2592 detail: "-1 is negative".into(),
2593 })
2594 );
2595 }
2596
2597 #[test]
2604 fn partition_records_frame_at_the_version_the_image_allows() {
2605 use crate::metadata_version::{DIRECTORY_ASSIGNMENT_MIN_LEVEL, ELR_MIN_LEVEL};
2606
2607 let rec = MetadataRecord::V1Partition(PartitionRecord {
2608 topic: "orders".into(),
2609 partition: 0,
2610 leader: NodeId(1),
2611 replicas: vec![NodeId(1)],
2612 isr: vec![NodeId(1)],
2613 leader_epoch: LeaderEpoch(7),
2614 adding_replicas: vec![],
2615 removing_replicas: vec![],
2616 directories: vec![],
2617 partition_epoch: 42,
2618 });
2619
2620 for (level, want) in [
2621 (ELR_MIN_LEVEL, 2_i16),
2622 (ELR_MIN_LEVEL - 1, 1),
2623 (DIRECTORY_ASSIGNMENT_MIN_LEVEL, 1),
2624 (DIRECTORY_ASSIGNMENT_MIN_LEVEL - 1, 0),
2625 ] {
2626 let mut image = img();
2627 image.apply(&MetadataRecord::V1Topic(TopicRecord {
2628 name: "orders".into(),
2629 topic_id: uuid::Uuid::from_u128(1),
2630 partitions: 1,
2631 replication_factor: 1,
2632 }));
2633 image.apply(&MetadataRecord::V1FeatureLevel(FeatureLevelRecord {
2634 name: METADATA_VERSION_FEATURE.into(),
2635 level,
2636 }));
2637
2638 let value = to_kraft_values(&rec, &image).unwrap().remove(0);
2639 let (_, version) = KraftMetadataRecord::decode_value(&value).unwrap();
2640 check!(version == want, "metadata.version {level}");
2641 }
2642 }
2643
2644 #[test]
2651 fn registration_features_and_endpoints_survive_translation() {
2652 let image = img();
2653
2654 let broker = MetadataRecord::V1BrokerRegistration(BrokerRegistrationRecord {
2655 node_id: NodeId(1),
2656 broker_epoch: 5,
2657 incarnation_id: uuid::Uuid::from_u128(3),
2658 host: "127.0.0.1".into(),
2659 port: 9092,
2660 rack: None,
2661 endpoints: vec![],
2662 log_dirs: vec![],
2663 fenced: false,
2664 in_controlled_shutdown: false,
2665 cordoned_log_dirs: None,
2666 features: BTreeMap::from([
2667 ("metadata.version".to_owned(), (7_i16, 25_i16)),
2668 ("kraft.version".to_owned(), (0, 1)),
2669 ]),
2670 });
2671 round_trip(&broker, &image);
2672
2673 let controller = MetadataRecord::V1ControllerRegistration(ControllerRegistrationRecord {
2674 node_id: NodeId(3),
2675 incarnation_id: uuid::Uuid::from_u128(9),
2676 zk_migration_ready: true,
2677 endpoints: vec![BrokerEndpoint {
2678 name: "CONTROLLER".into(),
2679 host: "controller-3".into(),
2680 port: 9093,
2681 protocol: krabka_security::ListenerProtocol::SaslSsl,
2682 }],
2683 features: BTreeMap::from([("kraft.version".to_owned(), (0_i16, 1_i16))]),
2684 });
2685 round_trip(&controller, &image);
2686 }
2687
2688 #[test]
2692 fn unregister_broker_carries_the_broker_epoch() {
2693 let rec = MetadataRecord::V1UnregisterBroker(UnregisterBrokerRecord {
2694 node_id: NodeId(42),
2695 broker_epoch: 0x0102,
2696 });
2697 let values = to_kraft_values(&rec, &img()).unwrap();
2698 check!(
2701 values.iter().map(|v| v.to_vec()).collect::<Vec<_>>()
2702 == vec![vec![
2703 0x01, 0x01, 0x00, 0, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0x01, 0x02, 0x00
2704 ]]
2705 );
2706 check!(from_kraft_value(&values[0], &img()) == Ok(rec));
2707 }
2708
2709 #[test]
2710 fn negative_wire_broker_id_is_rejected() {
2711 let record = KraftMetadataRecord::UnregisterBroker(KUnregisterBrokerRecord {
2712 broker_id: -1,
2713 ..Default::default()
2714 });
2715
2716 let error = from_kraft(&record, &img()).unwrap_err();
2717 assert!(matches!(error, TranslateError::InvalidValue { .. }));
2718 }
2719
2720 #[test]
2721 fn wire_partition_no_leader_sentinel_round_trips() {
2722 let mut image = img();
2723 let topic_id = uuid::Uuid::from_u128(100);
2724 image.apply(&MetadataRecord::V1Topic(TopicRecord {
2725 name: "leaderless-test".into(),
2726 topic_id,
2727 partitions: 1,
2728 replication_factor: 1,
2729 }));
2730 let record = KraftMetadataRecord::Partition(KPartitionRecord {
2731 topic_id: to_kuuid(topic_id),
2732 partition_id: 0,
2733 leader: -1,
2734 ..Default::default()
2735 });
2736
2737 let decoded = from_kraft(&record, &image).unwrap();
2738 let MetadataRecord::V1Partition(decoded) = decoded else {
2739 panic!("expected partition")
2740 };
2741 assert2::assert!(decoded.leader == NodeId(0));
2742
2743 let encoded = to_kraft(&MetadataRecord::V1Partition(decoded), &image).unwrap();
2744 let KraftMetadataRecord::Partition(encoded) = encoded else {
2745 panic!("expected partition")
2746 };
2747 assert2::assert!(encoded.leader == -1);
2748 }
2749
2750 #[test]
2751 fn wire_partition_change_no_leader_updates_epochs() {
2752 let mut image = img();
2753 let topic_id = uuid::Uuid::from_u128(101);
2754 image.apply(&MetadataRecord::V1Topic(TopicRecord {
2755 name: "leaderless-change-test".into(),
2756 topic_id,
2757 partitions: 1,
2758 replication_factor: 1,
2759 }));
2760 image.apply(&MetadataRecord::V1Partition(PartitionRecord {
2761 topic: "leaderless-change-test".into(),
2762 partition: 0,
2763 leader: NodeId(1),
2764 replicas: vec![NodeId(1)],
2765 isr: vec![NodeId(1)],
2766 leader_epoch: LeaderEpoch(7),
2767 partition_epoch: 11,
2768 adding_replicas: vec![],
2769 removing_replicas: vec![],
2770 directories: vec![],
2771 }));
2772 let change = KraftMetadataRecord::PartitionChange(KPartitionChangeRecord {
2773 topic_id: to_kuuid(topic_id),
2774 partition_id: 0,
2775 leader: -1,
2776 ..Default::default()
2777 });
2778
2779 let decoded = from_kraft(&change, &image).unwrap();
2780 let MetadataRecord::V1Partition(decoded) = decoded else {
2781 panic!("expected partition")
2782 };
2783 assert2::assert!(decoded.leader == NodeId(0));
2784 assert2::assert!(decoded.leader_epoch == LeaderEpoch(8));
2785 assert2::assert!(decoded.partition_epoch == 12);
2786 }
2787
2788 #[test]
2789 fn negative_wire_partition_replica_is_rejected() {
2790 let mut image = img();
2791 let topic_id = uuid::Uuid::from_u128(102);
2792 image.apply(&MetadataRecord::V1Topic(TopicRecord {
2793 name: "negative-node-test".into(),
2794 topic_id,
2795 partitions: 1,
2796 replication_factor: 1,
2797 }));
2798 let record = KraftMetadataRecord::Partition(KPartitionRecord {
2799 topic_id: to_kuuid(topic_id),
2800 partition_id: 0,
2801 leader: -1,
2802 replicas: vec![-1],
2803 ..Default::default()
2804 });
2805
2806 let error = from_kraft(&record, &image).unwrap_err();
2807 assert!(matches!(error, TranslateError::InvalidValue { .. }));
2808 }
2809
2810 #[test]
2811 fn scram_credential_round_trips() {
2812 let rec = MetadataRecord::V1ScramCredential(ScramCredentialRecord {
2813 user: "alice".into(),
2814 mechanism: SaslMechanism::ScramSha512,
2815 salt: vec![1u8; 16],
2816 stored_key: vec![2u8; 64],
2817 server_key: vec![3u8; 64],
2818 iterations: 4096,
2819 });
2820 round_trip(&rec, &img());
2821 }
2822
2823 #[test]
2824 fn delete_scram_credential_round_trips() {
2825 let rec = MetadataRecord::V1DeleteScramCredential(DeleteScramCredentialRecord {
2826 user: "alice".into(),
2827 mechanism: SaslMechanism::ScramSha256,
2828 });
2829 round_trip(&rec, &img());
2830 }
2831
2832 #[test]
2836 fn either_elr_field_alone_is_preserved() {
2837 let topic_id = uuid::Uuid::from_u128(1);
2838 let mut image = img();
2839 image.apply(&MetadataRecord::V1Topic(TopicRecord {
2840 name: "orders".into(),
2841 topic_id,
2842 partitions: 1,
2843 replication_factor: 1,
2844 }));
2845 image.apply(&MetadataRecord::V1Partition(PartitionRecord {
2846 topic: "orders".into(),
2847 partition: 0,
2848 leader: NodeId(1),
2849 replicas: vec![NodeId(1)],
2850 isr: vec![NodeId(1)],
2851 leader_epoch: LeaderEpoch(1),
2852 adding_replicas: vec![],
2853 removing_replicas: vec![],
2854 directories: vec![],
2855 partition_epoch: 1,
2856 }));
2857
2858 let change = |elr: Option<Vec<i32>>, last_known: Option<Vec<i32>>| {
2859 KraftMetadataRecord::PartitionChange(KPartitionChangeRecord {
2860 topic_id: to_kuuid(topic_id),
2861 partition_id: 0,
2862 leader_recovery_state: -1,
2863 eligible_leader_replicas: elr,
2864 last_known_elr: last_known,
2865 ..Default::default()
2866 })
2867 };
2868
2869 check!(from_kraft(&change(None, None), &image).is_ok());
2870 check!(matches!(
2871 from_kraft(&change(Some(vec![1]), None), &image),
2872 Ok(MetadataRecord::V1PartitionUpdate(crate::PartitionUpdateRecord {
2873 eligible_leader_replicas: Some(eligible_leader_replicas),
2874 last_known_elr: None,
2875 ..
2876 })) if eligible_leader_replicas == vec![NodeId(1)]
2877 ));
2878 check!(matches!(
2879 from_kraft(&change(None, Some(vec![1])), &image),
2880 Ok(MetadataRecord::V1PartitionUpdate(crate::PartitionUpdateRecord {
2881 eligible_leader_replicas: None,
2882 last_known_elr: Some(last_known_elr),
2883 ..
2884 })) if last_known_elr == vec![NodeId(1)]
2885 ));
2886
2887 let combined = KraftMetadataRecord::PartitionChange(KPartitionChangeRecord {
2888 topic_id: to_kuuid(topic_id),
2889 partition_id: 0,
2890 leader: 2,
2891 isr: Some(vec![2]),
2892 leader_recovery_state: LeaderRecoveryState::Recovering as i8,
2893 eligible_leader_replicas: Some(vec![2]),
2894 last_known_elr: Some(vec![1]),
2895 ..Default::default()
2896 });
2897 let decoded = from_kraft(&combined, &image).expect("combined partition delta");
2898 let MetadataRecord::V1PartitionUpdate(update) = decoded else {
2899 panic!("combined delta must stay atomic");
2900 };
2901 check!(update.partition.leader == NodeId(2));
2902 check!(update.partition.isr == vec![NodeId(2)]);
2903 check!(update.recovery_state == Some(LeaderRecoveryState::Recovering));
2904 check!(update.eligible_leader_replicas == Some(vec![NodeId(2)]));
2905 check!(update.last_known_elr == Some(vec![NodeId(1)]));
2906 }
2907
2908 #[test]
2914 fn distinct_acls_translate_to_distinct_ids() {
2915 let entry = |operation, resource_name: &str| crate::AclEntry {
2916 resource_type: ResourceType::Topic,
2917 resource_name: resource_name.into(),
2918 pattern_type: PatternType::Literal,
2919 principal: "User:alice".into(),
2920 host: "*".into(),
2921 operation,
2922 permission_type: PermissionType::Allow,
2923 };
2924 let id_of = |e: &crate::AclEntry| {
2925 let KraftMetadataRecord::AccessControlEntry(r) = acl_to_kraft(e) else {
2926 panic!("expected an access control entry");
2927 };
2928 r.id
2929 };
2930
2931 let read_foo = id_of(&entry(AclOperation::Read, "foo"));
2932 check!(read_foo == id_of(&entry(AclOperation::Read, "foo")));
2933 check!(read_foo != id_of(&entry(AclOperation::Write, "foo")));
2934 check!(read_foo != id_of(&entry(AclOperation::Read, "bar")));
2935 }
2936
2937 #[test]
2938 fn access_control_entry_round_trips() {
2939 let rec = MetadataRecord::V1AccessControlEntry(AclEntry {
2940 resource_type: ResourceType::Topic,
2941 resource_name: "foo".into(),
2942 pattern_type: PatternType::Literal,
2943 principal: "User:alice".into(),
2944 host: "*".into(),
2945 operation: AclOperation::Read,
2946 permission_type: PermissionType::Allow,
2947 });
2948 round_trip(&rec, &img());
2949 }
2950
2951 #[test]
2952 fn access_control_entry_two_phase_commit_round_trips() {
2953 let rec = MetadataRecord::V1AccessControlEntry(AclEntry {
2956 resource_type: ResourceType::TransactionalId,
2957 resource_name: "my-txn".into(),
2958 pattern_type: PatternType::Literal,
2959 principal: "User:flink".into(),
2960 host: "*".into(),
2961 operation: AclOperation::TwoPhaseCommit,
2962 permission_type: PermissionType::Allow,
2963 });
2964 round_trip(&rec, &img());
2965 }
2966
2967 #[test]
2972 fn acl_enums_map_to_kafka_wire_codes() {
2973 for (resource_type, wire) in [
2974 (ResourceType::Topic, 2_i8),
2975 (ResourceType::Group, 3),
2976 (ResourceType::Cluster, 4),
2977 (ResourceType::TransactionalId, 5),
2978 (ResourceType::DelegationToken, 6),
2979 (ResourceType::User, 7),
2980 ] {
2981 check!(
2982 (
2983 resource_type_to_wire(resource_type),
2984 resource_type_from_wire(wire).unwrap(),
2985 ) == (wire, resource_type)
2986 );
2987 }
2988 for (operation, wire) in [
2989 (AclOperation::All, 2_i8),
2990 (AclOperation::Read, 3),
2991 (AclOperation::Write, 4),
2992 (AclOperation::Create, 5),
2993 (AclOperation::Delete, 6),
2994 (AclOperation::Alter, 7),
2995 (AclOperation::Describe, 8),
2996 (AclOperation::ClusterAction, 9),
2997 (AclOperation::DescribeConfigs, 10),
2998 (AclOperation::AlterConfigs, 11),
2999 (AclOperation::IdempotentWrite, 12),
3000 (AclOperation::CreateTokens, 13),
3001 (AclOperation::DescribeTokens, 14),
3002 (AclOperation::TwoPhaseCommit, 15),
3003 ] {
3004 check!(
3005 (
3006 operation_to_wire(operation),
3007 operation_from_wire(wire).unwrap()
3008 ) == (wire, operation)
3009 );
3010 }
3011 check!(resource_type_from_wire(8).is_err());
3012 check!(operation_from_wire(16).is_err());
3013 }
3014
3015 #[test]
3018 fn access_control_entry_user_token_operations_round_trip() {
3019 for operation in [AclOperation::CreateTokens, AclOperation::DescribeTokens] {
3020 let rec = MetadataRecord::V1AccessControlEntry(AclEntry {
3021 resource_type: ResourceType::User,
3022 resource_name: "alice".into(),
3023 pattern_type: PatternType::Literal,
3024 principal: "User:token-requester".into(),
3025 host: "*".into(),
3026 operation,
3027 permission_type: PermissionType::Allow,
3028 });
3029 let value = to_kraft_values(&rec, &img()).unwrap().remove(0);
3030 check!(from_kraft_value(&value, &img()).unwrap() == rec);
3031 let KraftMetadataRecord::AccessControlEntry(wire) = to_kraft(&rec, &img()).unwrap()
3032 else {
3033 panic!("expected an access control entry");
3034 };
3035 check!((wire.resource_type, wire.operation) == (7, operation_to_wire(operation)));
3036 }
3037 }
3038
3039 #[test]
3040 fn access_control_entry_prefixed_deny_round_trips() {
3041 let rec = MetadataRecord::V1AccessControlEntry(AclEntry {
3042 resource_type: ResourceType::DelegationToken,
3043 resource_name: "User:bob".into(),
3044 pattern_type: PatternType::Prefixed,
3045 principal: "User:eve".into(),
3046 host: "10.0.0.1".into(),
3047 operation: AclOperation::Describe,
3048 permission_type: PermissionType::Deny,
3049 });
3050 round_trip(&rec, &img());
3051 }
3052
3053 fn acl(rt: ResourceType, name: &str, principal: &str) -> AclEntry {
3054 AclEntry {
3055 resource_type: rt,
3056 resource_name: name.into(),
3057 pattern_type: PatternType::Literal,
3058 principal: principal.into(),
3059 host: "*".into(),
3060 operation: AclOperation::Read,
3061 permission_type: PermissionType::Allow,
3062 }
3063 }
3064
3065 #[test]
3066 fn distinct_acls_get_distinct_ids() {
3067 let a = acl(ResourceType::Topic, "foo", "User:alice");
3068 let b = acl(ResourceType::Topic, "foo", "User:bob");
3069 let c = acl(ResourceType::Group, "foo", "User:alice");
3070 check!(acl_id(&a) != acl_id(&b));
3071 check!(acl_id(&a) != acl_id(&c));
3072 check!(acl_id(&a) == acl_id(&a.clone()));
3074 }
3075
3076 #[test]
3077 fn delete_acl_filter_resolves_to_remove_records_and_round_trips() {
3078 let mut image = img();
3081 let e1 = acl(ResourceType::Topic, "foo", "User:alice");
3082 let e2 = acl(ResourceType::Topic, "bar", "User:bob");
3083 let e3 = acl(ResourceType::Group, "cg", "User:alice");
3084 for e in [&e1, &e2, &e3] {
3085 image.apply(&MetadataRecord::V1AccessControlEntry(e.clone()));
3086 }
3087 let del = MetadataRecord::V1DeleteAccessControlEntry(AclEntryFilter {
3088 resource_type: Some(ResourceType::Topic),
3089 ..AclEntryFilter::default()
3090 });
3091 let records = to_kraft_records(&del, &image).unwrap();
3092 assert2::assert!(records.len() == 2); for k in &records {
3096 let back = from_kraft(k, &image).unwrap();
3097 image.apply(&back);
3098 }
3099 assert2::assert!(image.all_acls().count() == 1);
3100 let survivor = image.all_acls().next().unwrap();
3101 assert2::assert!(*survivor == e3);
3102 }
3103
3104 #[test]
3105 fn delete_acl_filter_matching_nothing_yields_no_records() {
3106 let mut image = img();
3107 image.apply(&MetadataRecord::V1AccessControlEntry(acl(
3108 ResourceType::Topic,
3109 "foo",
3110 "User:alice",
3111 )));
3112 let del = MetadataRecord::V1DeleteAccessControlEntry(AclEntryFilter {
3113 resource_type: Some(ResourceType::Group),
3114 ..AclEntryFilter::default()
3115 });
3116 assert2::assert!(to_kraft_records(&del, &image).unwrap().is_empty());
3117 }
3118
3119 #[test]
3120 fn client_quota_round_trips() {
3121 let rec = MetadataRecord::V1ClientQuota(ClientQuotaRecord {
3122 entity: vec![
3123 QuotaEntity {
3124 entity_type: "client-id".into(),
3125 entity_name: Some("app1".into()),
3126 },
3127 QuotaEntity {
3128 entity_type: "user".into(),
3129 entity_name: Some("alice".into()),
3130 },
3131 ],
3132 config_key: "producer_byte_rate".into(),
3133 config_value: Some(1024.0),
3134 });
3135 round_trip(&rec, &img());
3136 }
3137
3138 #[test]
3139 fn client_quota_remove_round_trips() {
3140 let rec = MetadataRecord::V1ClientQuota(ClientQuotaRecord {
3141 entity: vec![QuotaEntity {
3142 entity_type: "user".into(),
3143 entity_name: None,
3144 }],
3145 config_key: "consumer_byte_rate".into(),
3146 config_value: None,
3147 });
3148 round_trip(&rec, &img());
3149 }
3150
3151 #[test]
3152 fn delegation_token_round_trips() {
3153 let rec = MetadataRecord::V1DelegationToken(DelegationTokenRecord {
3154 token_id: "tok-abc".into(),
3155 owner: KafkaPrincipal {
3156 principal_type: "User".into(),
3157 name: "alice".into(),
3158 },
3159 requester: KafkaPrincipal {
3160 principal_type: "User".into(),
3161 name: "admin".into(),
3162 },
3163 issue_timestamp_ms: 1_700_000_000_000,
3164 expiry_timestamp_ms: 1_700_000_600_000,
3165 max_timestamp_ms: 1_700_604_800_000,
3166 renewers: vec![KafkaPrincipal {
3167 principal_type: "User".into(),
3168 name: "bob".into(),
3169 }],
3170 });
3171 round_trip(&rec, &img());
3172 }
3173
3174 #[test]
3175 fn delete_delegation_token_round_trips() {
3176 let rec = MetadataRecord::V1DeleteDelegationToken(DeleteDelegationTokenRecord {
3177 token_id: "tok-abc".into(),
3178 });
3179 round_trip(&rec, &img());
3180 }
3181
3182 #[test]
3183 fn broker_config_round_trips() {
3184 let rec = MetadataRecord::V1BrokerConfig(BrokerConfigRecord {
3185 node_id: NodeId(7),
3186 config_name: "leader.replication.throttled.rate".into(),
3187 config_value: Some("2048".into()),
3188 });
3189 let encoded = to_kraft_records(&rec, &img()).expect("encode broker config");
3190 assert2::assert!(matches!(
3191 encoded.as_slice(),
3192 [KraftMetadataRecord::Config(config)]
3193 if config.resource_type == 4 && config.resource_name == "7"
3194 ));
3195 round_trip(&rec, &img());
3196 }
3197
3198 #[test]
3199 fn default_broker_config_round_trips_with_empty_resource_name() {
3200 let rec = MetadataRecord::V1BrokerConfig(BrokerConfigRecord {
3201 node_id: DEFAULT_BROKER_CONFIG_NODE_ID,
3202 config_name: "follower.replication.throttled.rate".into(),
3203 config_value: Some("4096".into()),
3204 });
3205 let encoded = to_kraft_records(&rec, &img()).expect("encode default broker config");
3206 assert2::assert!(matches!(
3207 encoded.as_slice(),
3208 [KraftMetadataRecord::Config(config)]
3209 if config.resource_type == 4 && config.resource_name.is_empty()
3210 ));
3211 round_trip(&rec, &img());
3212 }
3213
3214 #[test]
3215 fn broker_config_delete_round_trips() {
3216 let rec = MetadataRecord::V1BrokerConfig(BrokerConfigRecord {
3217 node_id: NodeId(7),
3218 config_name: "leader.replication.throttled.rate".into(),
3219 config_value: None,
3220 });
3221 round_trip(&rec, &img());
3222 }
3223
3224 #[test]
3225 fn client_metrics_config_uses_standard_kraft_config_records() {
3226 let rec = MetadataRecord::V1ClientMetricsConfig(ClientMetricsConfigRecord {
3227 name: "metrics-sub-1".into(),
3228 configs: [
3229 ("interval.ms".to_owned(), "60000".to_owned()),
3230 ("metrics".to_owned(), "org.apache.kafka".to_owned()),
3231 ]
3232 .into_iter()
3233 .collect(),
3234 });
3235 let mut image = img();
3236 let records = to_kraft_records(&rec, &image).unwrap();
3237 assert2::assert!(records.len() == 2);
3238 assert2::assert!(records.iter().all(|record| matches!(
3239 record,
3240 KraftMetadataRecord::Config(config) if config.resource_type == 16
3241 )));
3242 for record in records {
3243 let decoded = from_kraft(&record, &image).unwrap();
3244 image.apply(&decoded);
3245 }
3246 assert2::assert!(
3247 image.client_metrics_config("metrics-sub-1")
3248 == match &rec {
3249 MetadataRecord::V1ClientMetricsConfig(config) => Some(&config.configs),
3250 _ => unreachable!(),
3251 }
3252 );
3253 }
3254
3255 #[test]
3256 fn client_metrics_config_clear_emits_standard_tombstones() {
3257 let mut image = img();
3258 image.apply(&MetadataRecord::V1ClientMetricsConfig(
3259 ClientMetricsConfigRecord {
3260 name: "metrics-sub-1".into(),
3261 configs: BTreeMap::from([
3262 ("interval.ms".into(), "60000".into()),
3263 ("metrics".into(), "org.apache.kafka".into()),
3264 ]),
3265 },
3266 ));
3267 let clear = MetadataRecord::V1ClientMetricsConfig(ClientMetricsConfigRecord {
3268 name: "metrics-sub-1".into(),
3269 configs: BTreeMap::new(),
3270 });
3271
3272 let records = to_kraft_records(&clear, &image).unwrap();
3273
3274 assert2::assert!(records.len() == 2);
3275 assert2::assert!(records.iter().all(|record| matches!(
3276 record,
3277 KraftMetadataRecord::Config(config)
3278 if config.resource_type == 16 && config.value.is_none()
3279 )));
3280 for record in records {
3281 let decoded = from_kraft(&record, &image).unwrap();
3282 image.apply(&decoded);
3283 }
3284 assert2::assert!(image.client_metrics_config("metrics-sub-1").is_none());
3285 }
3286
3287 #[test]
3288 fn features_epoch_round_trips_via_private_carrier() {
3289 let rec = MetadataRecord::V1FeaturesEpoch(FeaturesEpochRecord { epoch: 42 });
3293 let k = to_kraft(&rec, &img()).unwrap();
3294 assert2::assert!(matches!(
3295 k,
3296 KraftMetadataRecord::NoOp(no_op)
3297 if no_op.unknown_tagged_fields.0.len() == 1
3298 && no_op.unknown_tagged_fields.0[0].tag == PRIVATE_FEATURES_EPOCH_TAG
3299 ));
3300 round_trip(&rec, &img());
3301 }
3302
3303 #[test]
3304 fn partition_dir_assignment_uses_standard_partition_change_record() {
3305 let topic_id = uuid::Uuid::from_u128(0x00C0_FFEE);
3306 let mut image = img();
3307 image.apply(&MetadataRecord::V1Topic(TopicRecord {
3308 name: "t".into(),
3309 topic_id,
3310 partitions: 1,
3311 replication_factor: 3,
3312 }));
3313 image.apply(&MetadataRecord::V1Partition(PartitionRecord {
3314 topic: "t".into(),
3315 partition: 0,
3316 leader: NodeId(1),
3317 replicas: vec![NodeId(1), NodeId(2), NodeId(3)],
3318 isr: vec![NodeId(1), NodeId(2)],
3319 leader_epoch: LeaderEpoch(4),
3320 adding_replicas: vec![NodeId(3)],
3321 removing_replicas: vec![],
3322 directories: vec![uuid::Uuid::nil(); 3],
3323 partition_epoch: 9,
3324 }));
3325 let rec = MetadataRecord::V1PartitionDirAssignment(PartitionDirAssignmentRecord {
3326 topic: "t".into(),
3327 partition: 0,
3328 replica: NodeId(2),
3329 directory: uuid::Uuid::from_u128(0xAB),
3330 });
3331 let k = to_kraft(&rec, &image).unwrap();
3332 let KraftMetadataRecord::PartitionChange(change) = &k else {
3333 panic!("expected standard PartitionChangeRecord");
3334 };
3335 assert2::assert!(k.api_key() == 5);
3336 assert2::assert!(change.isr.is_none());
3337 assert2::assert!(change.replicas.is_none());
3338 assert2::assert!(
3339 change.directories.as_ref().unwrap()[1] == to_kuuid(uuid::Uuid::from_u128(0xAB))
3340 );
3341
3342 let values = to_kraft_values(&rec, &image).unwrap();
3343 let (decoded_wire, version) = KraftMetadataRecord::decode_value(&values[0]).unwrap();
3344 assert2::assert!(version == 2);
3345 let decoded = from_kraft(&decoded_wire, &image).unwrap();
3346 image.apply(&decoded);
3347 let partition = image.partition("t", 0).unwrap();
3348 assert2::assert!(partition.directories[1] == uuid::Uuid::from_u128(0xAB));
3349 assert2::assert!(partition.isr == vec![NodeId(1), NodeId(2)]);
3350 assert2::assert!(partition.adding_replicas == vec![NodeId(3)]);
3351 assert2::assert!(partition.leader_epoch == LeaderEpoch(4));
3352 assert2::assert!(partition.partition_epoch == 10);
3353 }
3354
3355 #[test]
3356 fn voters_and_kraft_version_have_no_counterpart() {
3357 let v = MetadataRecord::V1Voters(VotersRecord {
3358 voters: VoterSet::default(),
3359 });
3360 let k = MetadataRecord::V1KRaftVersion(KRaftVersionRecord { kraft_version: 1 });
3361 for (_name, record, expected) in [
3362 ("voter record", v, TranslateError::NoCounterpart("V1Voters")),
3363 (
3364 "KRaft version record",
3365 k,
3366 TranslateError::NoCounterpart("V1KRaftVersion"),
3367 ),
3368 ] {
3369 assert2::assert!(to_kraft(&record, &img()) == Err(expected));
3370 }
3371 }
3372
3373 #[test]
3376 fn topic_partition_config_round_trip_with_image_context() {
3377 let mut image = img();
3378 let tid = uuid::Uuid::from_u128(7);
3379 let topic = MetadataRecord::V1Topic(TopicRecord {
3380 name: "t".into(),
3381 topic_id: tid,
3382 partitions: 1,
3383 replication_factor: 1,
3384 });
3385 image.apply(&topic);
3386 let part = MetadataRecord::V1Partition(PartitionRecord {
3387 topic: "t".into(),
3388 partition: 0,
3389 leader: NodeId(1),
3390 replicas: vec![NodeId(1)],
3391 isr: vec![NodeId(1)],
3392 leader_epoch: LeaderEpoch(0),
3393 adding_replicas: vec![],
3394 removing_replicas: vec![],
3395 directories: vec![],
3396 partition_epoch: 3,
3397 });
3398 image.apply(&part);
3399 for rec in [&topic, &part] {
3400 let k = to_kraft(rec, &image).unwrap();
3401 assert2::assert!(from_kraft(&k, &image).unwrap() == *rec);
3402 }
3403 let cfg = MetadataRecord::V1TopicConfig(TopicConfigRecord {
3404 topic: "t".into(),
3405 overrides: [("retention.ms".to_owned(), "9".to_owned())].into(),
3406 });
3407 let k = to_kraft(&cfg, &image).unwrap();
3408 assert2::assert!(from_kraft(&k, &image).unwrap() == cfg);
3409 }
3410
3411 #[test]
3412 fn delete_topic_round_trips_with_image_context() {
3413 let mut image = img();
3414 let tid = uuid::Uuid::from_u128(11);
3415 image.apply(&MetadataRecord::V1Topic(TopicRecord {
3416 name: "doomed".into(),
3417 topic_id: tid,
3418 partitions: 1,
3419 replication_factor: 1,
3420 }));
3421 let rec = MetadataRecord::V1DeleteTopic(DeleteTopicRecord {
3422 name: "doomed".into(),
3423 });
3424 round_trip(&rec, &image);
3425 }
3426
3427 #[test]
3428 fn topic_config_two_keys_yields_two_config_records() {
3429 let mut image = img();
3430 image.apply(&MetadataRecord::V1Topic(TopicRecord {
3431 name: "t".into(),
3432 topic_id: uuid::Uuid::from_u128(7),
3433 partitions: 1,
3434 replication_factor: 1,
3435 }));
3436 let cfg = MetadataRecord::V1TopicConfig(TopicConfigRecord {
3437 topic: "t".into(),
3438 overrides: [
3439 ("retention.ms".to_owned(), "9".to_owned()),
3440 ("segment.bytes".to_owned(), "1048576".to_owned()),
3441 ]
3442 .into(),
3443 });
3444 let records = to_kraft_records(&cfg, &image).unwrap();
3445 assert2::assert!(records.len() == 2);
3446 for k in &records {
3448 let back = from_kraft(k, &image).unwrap();
3449 match back {
3450 MetadataRecord::V1TopicConfig(tc) => {
3451 assert2::assert!(tc.topic.as_str() == "t");
3452 assert2::assert!(tc.overrides.len() == 1);
3453 }
3454 other => panic!("expected V1TopicConfig singleton, got {other:?}"),
3455 }
3456 }
3457 }
3458
3459 #[test]
3460 fn topic_config_to_kraft_preserves_set_values() {
3461 let mut image = img();
3462 image.apply(&MetadataRecord::V1Topic(TopicRecord {
3463 name: "t".into(),
3464 topic_id: uuid::Uuid::from_u128(7),
3465 partitions: 1,
3466 replication_factor: 1,
3467 }));
3468 let cfg = MetadataRecord::V1TopicConfig(TopicConfigRecord {
3469 topic: "t".into(),
3470 overrides: [("retention.ms".to_owned(), "9".to_owned())].into(),
3471 });
3472 let records = to_kraft_records(&cfg, &image).unwrap();
3473 assert2::assert!(records.len() == 1);
3474 let KraftMetadataRecord::Config(c) = &records[0] else {
3475 panic!("expected Config");
3476 };
3477 assert2::assert!(c.name.as_str() == "retention.ms");
3478 assert2::assert!(c.value.as_deref() == Some("9"));
3479 }
3480
3481 fn seed_topic_config(image: &mut MetadataImage, kv: &[(&str, &str)]) {
3483 image.apply(&MetadataRecord::V1Topic(TopicRecord {
3484 name: "t".into(),
3485 topic_id: uuid::Uuid::from_u128(7),
3486 partitions: 1,
3487 replication_factor: 1,
3488 }));
3489 image.apply(&MetadataRecord::V1TopicConfig(TopicConfigRecord {
3490 topic: "t".into(),
3491 overrides: kv
3492 .iter()
3493 .map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
3494 .collect(),
3495 }));
3496 }
3497
3498 #[test]
3499 fn topic_config_change_emits_set_and_tombstone_and_merges_back() {
3500 let mut image = img();
3502 seed_topic_config(&mut image, &[("a", "1"), ("b", "2")]);
3503 let submit = MetadataRecord::V1TopicConfig(TopicConfigRecord {
3504 topic: "t".into(),
3505 overrides: [("a".to_owned(), "9".to_owned())].into(),
3506 });
3507 let records = to_kraft_records(&submit, &image).unwrap();
3508 assert2::assert!(records.len() == 2);
3510 for k in &records {
3513 let back = from_kraft(k, &image).unwrap();
3514 image.apply(&back);
3515 }
3516 let want: BTreeMap<String, String> = [("a".to_owned(), "9".to_owned())].into();
3517 assert2::assert!(image.topic_config("t") == Some(&want));
3518 }
3519
3520 #[test]
3521 fn topic_config_clear_emits_tombstones_for_every_key() {
3522 let mut image = img();
3524 seed_topic_config(&mut image, &[("a", "1"), ("b", "2")]);
3525 let submit = MetadataRecord::V1TopicConfig(TopicConfigRecord {
3526 topic: "t".into(),
3527 overrides: BTreeMap::new(),
3528 });
3529 let records = to_kraft_records(&submit, &image).unwrap();
3530 assert2::assert!(records.len() == 2);
3531 for k in &records {
3532 let back = from_kraft(k, &image).unwrap();
3533 image.apply(&back);
3534 }
3535 assert2::assert!(image.topic_config("t").is_none());
3537 }
3538
3539 #[test]
3540 fn group_config_change_uses_kraft_group_records_and_merges_back() {
3541 let mut image = img();
3542 image.apply(&MetadataRecord::V1GroupConfig(GroupConfigRecord {
3543 group_id: "streams-app".into(),
3544 configs: BTreeMap::from([
3545 ("streams.num.standby.replicas".into(), "0".into()),
3546 ("streams.num.warmup.replicas".into(), "2".into()),
3547 ]),
3548 }));
3549 let submitted = MetadataRecord::V1GroupConfig(GroupConfigRecord {
3550 group_id: "streams-app".into(),
3551 configs: BTreeMap::from([("streams.num.standby.replicas".into(), "1".into())]),
3552 });
3553
3554 let records = to_kraft_records(&submitted, &image).expect("translate group config");
3555 assert2::assert!(records.len() == 2);
3556 assert2::assert!(records.iter().all(|record| matches!(
3557 record,
3558 KraftMetadataRecord::Config(config) if config.resource_type == 32
3559 )));
3560 for record in records {
3561 let decoded = from_kraft(&record, &image).expect("decode group config");
3562 image.apply(&decoded);
3563 }
3564 assert2::assert!(
3565 image.group_config("streams-app")
3566 == match &submitted {
3567 MetadataRecord::V1GroupConfig(config) => Some(&config.configs),
3568 _ => unreachable!(),
3569 }
3570 );
3571 }
3572
3573 #[test]
3574 fn producer_ids_use_standard_kraft_record() {
3575 let image = img();
3576 let submitted = MetadataRecord::V1ProducerIds(ProducerIdsRecord {
3577 broker_id: NodeId(3),
3578 broker_epoch: 11,
3579 next_producer_id: 5_000,
3580 });
3581 let values = to_kraft_values(&submitted, &image).expect("encode producer IDs record");
3582 assert2::assert!(values.len() == 1);
3583 let (wire, version) = KraftMetadataRecord::decode_value(&values[0]).unwrap();
3584 assert2::assert!(wire.api_key() == 15);
3585 assert2::assert!(version == 0);
3586 assert2::assert!(from_kraft(&wire, &image).unwrap() == submitted);
3587 }
3588
3589 #[test]
3590 fn to_kraft_values_round_trips_through_value_bytes() {
3591 let image = img();
3594 let rec = MetadataRecord::V1FeatureLevel(FeatureLevelRecord {
3595 name: "metadata.version".into(),
3596 level: 25,
3597 });
3598 let values = to_kraft_values(&rec, &image).unwrap();
3599 assert2::assert!(values.len() == 1);
3600 let back = from_kraft_value(&values[0], &image).unwrap();
3601 assert2::assert!(back == rec);
3602 }
3603
3604 #[test]
3605 fn partition_record_directories_survive_kraft_round_trip() {
3606 use crate::{MetadataImage, MetadataRecord, PartitionRecord, TopicRecord};
3607 let topic_id = uuid::Uuid::from_u128(0x11);
3608 let mut image = MetadataImage::new(uuid::Uuid::nil());
3609 image.apply(&MetadataRecord::V1Topic(TopicRecord {
3610 name: "t".into(),
3611 topic_id,
3612 partitions: 1,
3613 replication_factor: 2,
3614 }));
3615 let rec = MetadataRecord::V1Partition(PartitionRecord {
3616 topic: "t".into(),
3617 partition: 0,
3618 leader: NodeId(1),
3619 replicas: vec![NodeId(1), NodeId(2)],
3620 isr: vec![NodeId(1), NodeId(2)],
3621 leader_epoch: LeaderEpoch(4),
3622 adding_replicas: vec![],
3623 removing_replicas: vec![],
3624 directories: vec![uuid::Uuid::from_u128(0xAA), uuid::Uuid::from_u128(0xBB)],
3625 partition_epoch: 0,
3626 });
3627 let values = super::to_kraft_values(&rec, &image).expect("encode");
3628 assert2::assert!(values.len() == 1);
3629 let decoded = super::from_kraft_value(&values[0], &image).expect("decode");
3630 assert2::assert!(decoded == rec);
3631 }
3632
3633 #[test]
3634 fn offset_advance_round_trips_through_carrier() {
3635 let rec = MetadataRecord::V1PartitionOffsetAdvance(crate::PartitionOffsetAdvanceRecord {
3636 topic: "t".into(),
3637 partition: 2,
3638 count: 9,
3639 });
3640
3641 let values = to_kraft_values(&rec, &img()).unwrap();
3642 assert_eq!(values.len(), 1);
3643 let back = from_kraft_value(&values[0], &img()).unwrap();
3644
3645 assert_eq!(back, rec);
3646 }
3647
3648 fn freeze_records() -> [(&'static str, MetadataRecord, u32); 3] {
3651 let id = uuid::Uuid::from_u128(0xB1);
3652 [
3653 (
3654 "topic freeze",
3655 MetadataRecord::V1TopicFreeze(TopicFreezeRecord {
3656 scope: "tenant-a.".into(),
3657 pattern_type: PatternType::Prefixed,
3658 frozen: true,
3659 reason: "DR cutover".into(),
3660 set_by: "User:alice".into(),
3661 set_at_ms: 1_700_000_000_000,
3662 proposal_id: id,
3663 key_id: "alice-yubi".into(),
3664 signature: vec![0xAB; 64],
3665 }),
3666 PRIVATE_TOPIC_FREEZE_TAG,
3667 ),
3668 (
3669 "break-glass proposal",
3670 MetadataRecord::V1BreakGlassProposal(BreakGlassProposalRecord {
3671 proposal_id: id,
3672 action: BreakGlassAction::ThawTopicFreeze,
3673 target: "literal:orders".into(),
3674 proposer: "User:alice".into(),
3675 reason: "incident 4711 closed".into(),
3676 created_at_ms: 1_700_000_000_000,
3677 expires_at_ms: 1_700_000_600_000,
3678 approvals: vec![BreakGlassApproval {
3679 principal: "User:bob".into(),
3680 approved_at_ms: 1_700_000_060_000,
3681 key_id: "bob-yubi".into(),
3682 signature: vec![0xCD; 64],
3683 }],
3684 consumed_at_ms: 1_700_000_120_000,
3685 withdrawn: false,
3686 }),
3687 PRIVATE_BREAK_GLASS_PROPOSAL_TAG,
3688 ),
3689 (
3690 "break-glass tombstone",
3691 MetadataRecord::V1DeleteBreakGlassProposal(id),
3692 PRIVATE_DELETE_BREAK_GLASS_PROPOSAL_TAG,
3693 ),
3694 ]
3695 }
3696
3697 #[test]
3698 fn freeze_and_break_glass_records_round_trip_through_carriers() {
3699 for (label, rec, _) in freeze_records() {
3700 let values = to_kraft_values(&rec, &img()).unwrap();
3701 check!(values.len() == 1, "{label}");
3702 check!(
3703 from_kraft_value(&values[0], &img()).unwrap() == rec,
3704 "{label}"
3705 );
3706 }
3707 }
3708
3709 #[test]
3712 fn freeze_and_break_glass_carriers_use_the_pinned_private_tags() {
3713 for (label, rec, want) in freeze_records() {
3714 let KraftMetadataRecord::NoOp(no_op) = to_kraft(&rec, &img()).unwrap() else {
3715 panic!("{label}: expected a private NoOpRecord carrier");
3716 };
3717 let tags: Vec<u32> = no_op
3718 .unknown_tagged_fields
3719 .0
3720 .iter()
3721 .map(|f| f.tag)
3722 .collect();
3723 check!(tags == vec![want], "{label}");
3724 }
3725 check!(
3726 PRIVATE_TAGS
3727 == [
3728 PRIVATE_FEATURES_EPOCH_TAG,
3729 PRIVATE_PARTITION_OFFSET_ADVANCE_TAG,
3730 PRIVATE_TOPIC_FREEZE_TAG,
3731 PRIVATE_BREAK_GLASS_PROPOSAL_TAG,
3732 PRIVATE_DELETE_BREAK_GLASS_PROPOSAL_TAG,
3733 ]
3734 );
3735 check!(PRIVATE_TAGS == [1001, 1003, 1004, 1005, 1006]);
3736 }
3737
3738 const KAFKA_METADATA_RECORD_API_KEYS: [u32; 27] = [
3743 0, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 14, 15, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
3744 28, 29,
3745 ];
3746
3747 fn private_rows() -> Vec<(&'static str, MetadataRecord, u32)> {
3749 let mut rows: Vec<(&'static str, MetadataRecord, u32)> = vec![
3750 (
3751 "features epoch",
3752 MetadataRecord::V1FeaturesEpoch(FeaturesEpochRecord { epoch: 7 }),
3753 PRIVATE_FEATURES_EPOCH_TAG,
3754 ),
3755 (
3756 "diskless offset advance",
3757 MetadataRecord::V1PartitionOffsetAdvance(crate::PartitionOffsetAdvanceRecord {
3758 topic: "t".into(),
3759 partition: 2,
3760 count: 9,
3761 }),
3762 PRIVATE_PARTITION_OFFSET_ADVANCE_TAG,
3763 ),
3764 ];
3765 rows.extend(freeze_records());
3766 rows
3767 }
3768
3769 fn versioned_body(version: i16, rec: &MetadataRecord) -> Vec<u8> {
3772 let mut body = version.to_be_bytes().to_vec();
3773 body.extend(<serde_wincode::SerdeCompat<MetadataRecord>>::serialize(rec).unwrap());
3774 body
3775 }
3776
3777 #[test]
3782 fn private_records_frame_as_a_kafka_no_op_record() {
3783 for (label, rec, tag) in private_rows() {
3784 let values = to_kraft_values(&rec, &img()).unwrap();
3785 check!(values.len() == 1, "{label}");
3786 let (decoded, version) = KraftMetadataRecord::decode_value(&values[0]).unwrap();
3787 check!(
3788 KAFKA_METADATA_RECORD_API_KEYS.contains(&decoded.api_key()),
3789 "{label}: apiKey {} is not in Kafka's table",
3790 decoded.api_key()
3791 );
3792 check!(
3793 (decoded, version)
3794 == (
3795 KraftMetadataRecord::NoOp(NoOpRecord {
3796 unknown_tagged_fields: UnknownTaggedFields(vec![UnknownTaggedField {
3797 tag,
3798 bytes: Bytes::from(versioned_body(PRIVATE_RECORD_VERSION, &rec)),
3799 }]),
3800 }),
3801 0
3802 ),
3803 "{label}"
3804 );
3805 check!(
3806 from_kraft_value(&values[0], &img()).unwrap() == rec,
3807 "{label}"
3808 );
3809 }
3810 }
3811
3812 const PRIVATE_GOLDEN_VALUES: [&str; 5] = [
3817 "01140001e9070e0000130000000700000000000000",
3819 "01140001eb071b000015000000010000000000000074020000000900000000000000",
3821 "01140001ec07ba01000018000000090000000000000074656e616e742d612e01000000010a000000000000004452206375746f7665720a00000000000000557365723a616c6963650068e5cf8b0100001000000000000000000000000000000000000000000000b10a00000000000000616c6963652d797562694000000000000000abababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababab",
3823 "01140001ed07f7010000190000001000000000000000000000000000000000000000000000b1000000000e000000000000006c69746572616c3a6f72646572730a00000000000000557365723a616c6963651400000000000000696e636964656e74203437313120636c6f7365640068e5cf8b010000c08feecf8b01000001000000000000000800000000000000557365723a626f626052e6cf8b0100000800000000000000626f622d797562694000000000000000cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdc03ce7cf8b01000000",
3825 "01140001ee071e00001a0000001000000000000000000000000000000000000000000000b1",
3827 ];
3828
3829 #[test]
3830 fn private_records_match_the_golden_bytes() {
3831 use crate::wincode_contract::{hex, unhex};
3832
3833 for ((label, rec, _), golden) in private_rows().into_iter().zip(PRIVATE_GOLDEN_VALUES) {
3834 let values = to_kraft_values(&rec, &img()).unwrap();
3835 check!(
3836 values.iter().map(|v| hex(v)).collect::<Vec<_>>() == vec![golden.to_owned()],
3837 "{label}"
3838 );
3839 check!(
3840 from_kraft_value(&unhex(golden), &img()) == Ok(rec),
3841 "{label}"
3842 );
3843 }
3844 }
3845
3846 #[test]
3847 fn private_carrier_rejects_a_bad_tag_or_body() {
3848 let epoch = MetadataRecord::V1FeaturesEpoch(FeaturesEpochRecord { epoch: 7 });
3849 let topic = MetadataRecord::V1Topic(TopicRecord {
3850 name: "t".into(),
3851 topic_id: uuid::Uuid::from_u128(0x7),
3852 partitions: 1,
3853 replication_factor: 1,
3854 });
3855 let unversioned = <serde_wincode::SerdeCompat<MetadataRecord>>::serialize(&epoch).unwrap();
3856 for (label, tag, body, want) in [
3857 (
3858 "version 1",
3859 1001,
3860 versioned_body(1, &epoch),
3861 TranslateError::UnknownPrivateRecordVersion {
3862 tag: 1001,
3863 version: 1,
3864 },
3865 ),
3866 (
3867 "future version",
3868 1001,
3869 versioned_body(i16::MAX, &epoch),
3870 TranslateError::UnknownPrivateRecordVersion {
3871 tag: 1001,
3872 version: i16::MAX,
3873 },
3874 ),
3875 (
3876 "0.x body without a version",
3877 1001,
3878 unversioned,
3879 TranslateError::UnknownPrivateRecordVersion {
3880 tag: 1001,
3881 version: 0x1300,
3882 },
3883 ),
3884 (
3885 "record of another tag",
3886 1004,
3887 versioned_body(0, &epoch),
3888 TranslateError::PrivateTagMismatch {
3889 tag: 1004,
3890 expected_tag: Some(1001),
3891 },
3892 ),
3893 (
3894 "record of no tag",
3895 1001,
3896 versioned_body(0, &topic),
3897 TranslateError::PrivateTagMismatch {
3898 tag: 1001,
3899 expected_tag: None,
3900 },
3901 ),
3902 (
3903 "trailing byte after the record",
3904 1001,
3905 {
3906 let mut body = versioned_body(0, &epoch);
3907 body.push(0);
3908 body
3909 },
3910 TranslateError::TrailingPrivateRecordBytes {
3911 tag: 1001,
3912 trailing: 1,
3913 },
3914 ),
3915 (
3916 "burned tag 1002",
3917 1002,
3918 versioned_body(0, &epoch),
3919 TranslateError::UnknownPrivateTag(1002),
3920 ),
3921 (
3922 "unassigned tag 1007",
3923 1007,
3924 versioned_body(0, &epoch),
3925 TranslateError::UnknownPrivateTag(1007),
3926 ),
3927 (
3928 "one-byte body",
3929 1001,
3930 vec![0],
3931 TranslateError::Decode(
3932 "krabka-private NoOpRecord tag 1001 body is 1 bytes, too short for its i16 \
3933 version"
3934 .into(),
3935 ),
3936 ),
3937 ] {
3938 let no_op = KraftMetadataRecord::NoOp(NoOpRecord {
3939 unknown_tagged_fields: UnknownTaggedFields(vec![UnknownTaggedField {
3940 tag,
3941 bytes: Bytes::from(body),
3942 }]),
3943 });
3944 check!(from_kraft(&no_op, &img()) == Err(want), "{label}");
3945 }
3946 }
3947
3948 #[test]
3951 fn no_op_without_one_private_tag_has_no_counterpart() {
3952 let field = |tag: u32| UnknownTaggedField {
3953 tag,
3954 bytes: Bytes::from_static(b"x"),
3955 };
3956 for (label, fields) in [
3957 ("empty", vec![]),
3958 ("foreign tag", vec![field(7)]),
3959 (
3960 "two private tags",
3961 vec![
3962 field(PRIVATE_TOPIC_FREEZE_TAG),
3963 field(PRIVATE_BREAK_GLASS_PROPOSAL_TAG),
3964 ],
3965 ),
3966 ] {
3967 let no_op = KraftMetadataRecord::NoOp(NoOpRecord {
3968 unknown_tagged_fields: UnknownTaggedFields(fields),
3969 });
3970 check!(
3971 from_kraft(&no_op, &img()) == Err(TranslateError::NoCounterpart("NoOp")),
3972 "{label}"
3973 );
3974 }
3975 }
3976
3977 #[test]
3978 fn partition_to_kraft_preserves_reassignment_vectors() {
3979 let topic_id = uuid::Uuid::from_u128(0x11);
3980 let mut image = MetadataImage::new(uuid::Uuid::nil());
3981 image.apply(&MetadataRecord::V1Topic(TopicRecord {
3982 name: "t".into(),
3983 topic_id,
3984 partitions: 1,
3985 replication_factor: 3,
3986 }));
3987 let rec = MetadataRecord::V1Partition(PartitionRecord {
3988 topic: "t".into(),
3989 partition: 0,
3990 leader: NodeId(1),
3991 replicas: vec![NodeId(1), NodeId(2), NodeId(3)],
3992 isr: vec![NodeId(1), NodeId(2)],
3993 leader_epoch: LeaderEpoch(4),
3994 adding_replicas: vec![NodeId(3)],
3995 removing_replicas: vec![NodeId(2)],
3996 directories: vec![],
3997 partition_epoch: 9,
3998 });
3999
4000 let KraftMetadataRecord::Partition(k) = to_kraft(&rec, &image).unwrap() else {
4001 panic!("expected Partition");
4002 };
4003
4004 assert2::assert!(k.adding_replicas == vec![3]);
4005 assert2::assert!(k.removing_replicas == vec![2]);
4006 }
4007
4008 #[test]
4009 fn non_private_unknown_has_no_counterpart() {
4010 let err = from_kraft(
4011 &KraftMetadataRecord::Unknown {
4012 api_key: 999_999,
4013 api_version: 0,
4014 body: bytes::Bytes::from_static(b"not a private carrier"),
4015 },
4016 &img(),
4017 )
4018 .unwrap_err();
4019
4020 assert2::assert!(err == TranslateError::NoCounterpart("Unknown"));
4021 }
4022
4023 #[test]
4024 fn remove_acl_with_unknown_id_is_rejected() {
4025 let remove_unknown =
4026 KraftMetadataRecord::RemoveAccessControlEntry(RemoveAccessControlEntryRecord {
4027 id: to_kuuid(uuid::Uuid::from_u128(0xA11CE)),
4028 ..Default::default()
4029 });
4030 let err = from_kraft(&remove_unknown, &img()).unwrap_err();
4031
4032 assert2::assert!(matches!(err, TranslateError::UnknownAclId(_)));
4033
4034 let mut image_with_different_acl = img();
4035 image_with_different_acl.apply(&MetadataRecord::V1AccessControlEntry(acl(
4036 ResourceType::Topic,
4037 "topic-a",
4038 "User:alice",
4039 )));
4040 let err = from_kraft(&remove_unknown, &image_with_different_acl).unwrap_err();
4041
4042 assert2::assert!(matches!(err, TranslateError::UnknownAclId(_)));
4043 }
4044
4045 #[test]
4046 fn kraft_variant_name_reports_unmodeled_variants() {
4047 for (record, want) in [
4048 (
4049 KraftMetadataRecord::BrokerRegistrationChange(
4050 BrokerRegistrationChangeRecord::default(),
4051 ),
4052 "BrokerRegistrationChange",
4053 ),
4054 (KraftMetadataRecord::NoOp(NoOpRecord::default()), "NoOp"),
4055 (
4056 KraftMetadataRecord::BeginTransaction(BeginTransactionRecord::default()),
4057 "BeginTransaction",
4058 ),
4059 (
4060 KraftMetadataRecord::EndTransaction(EndTransactionRecord::default()),
4061 "EndTransaction",
4062 ),
4063 (
4064 KraftMetadataRecord::RemoveAccessControlEntry(
4065 RemoveAccessControlEntryRecord::default(),
4066 ),
4067 "RemoveAccessControlEntry",
4068 ),
4069 (
4070 KraftMetadataRecord::PartitionChange(KPartitionChangeRecord::default()),
4071 "PartitionChange",
4072 ),
4073 (
4074 KraftMetadataRecord::Unknown {
4075 api_key: 42,
4076 api_version: 0,
4077 body: bytes::Bytes::new(),
4078 },
4079 "Unknown",
4080 ),
4081 ] {
4082 assert2::assert!(kraft_variant_name(&record) == want);
4083 }
4084 }
4085
4086 #[test]
4087 fn image_derives_partition_count_from_partitions_map() {
4088 let mut image = img();
4089 image.apply(&MetadataRecord::V1Topic(TopicRecord {
4090 name: "t".into(),
4091 topic_id: uuid::Uuid::from_u128(7),
4092 partitions: 3,
4093 replication_factor: 1,
4094 }));
4095 for p in 0..3 {
4096 image.apply(&MetadataRecord::V1Partition(PartitionRecord {
4097 topic: "t".into(),
4098 partition: p,
4099 leader: NodeId(1),
4100 replicas: vec![NodeId(1)],
4101 isr: vec![NodeId(1)],
4102 leader_epoch: LeaderEpoch(0),
4103 adding_replicas: vec![],
4104 removing_replicas: vec![],
4105 directories: vec![],
4106 partition_epoch: 0,
4107 }));
4108 }
4109 assert2::assert!(image.topic_partition_count("t") == 3);
4110 }
4111
4112 fn classification_image() -> (MetadataImage, uuid::Uuid) {
4116 let topic_id = uuid::Uuid::from_u128(0x7E);
4117 let mut image = img();
4118 image.apply(&MetadataRecord::V1Topic(TopicRecord {
4119 name: "t".into(),
4120 topic_id,
4121 partitions: 3,
4122 replication_factor: 1,
4123 }));
4124 for (partition, leader_epoch, partition_epoch) in [
4125 (0, LeaderEpoch(7), 3),
4126 (1, LeaderEpoch(i32::MAX), 3),
4127 (2, LeaderEpoch(7), i32::MAX),
4128 ] {
4129 image.apply(&MetadataRecord::V1Partition(PartitionRecord {
4130 topic: "t".into(),
4131 partition,
4132 leader: NodeId(1),
4133 replicas: vec![NodeId(1)],
4134 isr: vec![NodeId(1)],
4135 leader_epoch,
4136 partition_epoch,
4137 adding_replicas: vec![],
4138 removing_replicas: vec![],
4139 directories: vec![],
4140 }));
4141 }
4142 (image, topic_id)
4143 }
4144
4145 #[test]
4149 fn invalid_errors_split_into_reference_and_value() {
4150 let (image, topic_id) = classification_image();
4151 let change = |partition_id: i32, edit: fn(&mut KPartitionChangeRecord)| {
4152 let mut record = KPartitionChangeRecord {
4153 topic_id: to_kuuid(topic_id),
4154 partition_id,
4155 ..Default::default()
4156 };
4157 edit(&mut record);
4158 from_kraft(&KraftMetadataRecord::PartitionChange(record), &image)
4159 };
4160 let reference = |field, detail: &str| {
4161 Err(TranslateError::InvalidReference {
4162 field,
4163 detail: detail.into(),
4164 })
4165 };
4166 let value = |field, detail: &str| {
4167 Err(TranslateError::InvalidValue {
4168 field,
4169 detail: detail.into(),
4170 })
4171 };
4172
4173 for (case, got, want) in [
4174 (
4175 "decode: unknown partition",
4176 change(5, |_| {}),
4177 reference("partition change", "unknown partition t-5"),
4178 ),
4179 (
4180 "decode: directories do not match the image's replicas",
4181 change(0, |c| {
4182 c.directories = Some(vec![KUuid([1; 16]), KUuid([2; 16])]);
4183 }),
4184 reference(
4185 "partition change directories",
4186 "2 directories for 1 replicas",
4187 ),
4188 ),
4189 (
4190 "decode: directories do not match replicas in the same record",
4191 change(0, |c| {
4192 c.replicas = Some(vec![1, 2]);
4193 c.directories = Some(vec![KUuid([1; 16])]);
4194 }),
4195 value(
4196 "partition change directories",
4197 "1 directories for 2 replicas",
4198 ),
4199 ),
4200 (
4201 "decode: image leader epoch at its maximum",
4202 change(1, |c| c.leader = 1),
4203 reference("partition change leader epoch", "leader epoch overflow"),
4204 ),
4205 (
4206 "encode: directory assignment for an unknown partition",
4207 to_kraft(
4208 &MetadataRecord::V1PartitionDirAssignment(PartitionDirAssignmentRecord {
4209 topic: "t".into(),
4210 partition: 9,
4211 replica: NodeId(1),
4212 directory: uuid::Uuid::from_u128(0xD1),
4213 }),
4214 &image,
4215 )
4216 .map(|_| unreachable!()),
4217 reference("partition directory assignment", "unknown partition t-9"),
4218 ),
4219 (
4220 "decode: image partition epoch at its maximum",
4221 change(2, |_| {}),
4222 reference(
4223 "partition change partition epoch",
4224 "partition epoch overflow",
4225 ),
4226 ),
4227 (
4228 "decode: unknown leader recovery state on a partition record",
4229 from_kraft(
4230 &KraftMetadataRecord::Partition(KPartitionRecord {
4231 topic_id: to_kuuid(topic_id),
4232 partition_id: 0,
4233 leader: 1,
4234 replicas: vec![1],
4235 isr: vec![1],
4236 leader_recovery_state: 9,
4237 ..Default::default()
4238 }),
4239 &image,
4240 ),
4241 value("partition leader recovery state", "unknown state 9"),
4242 ),
4243 (
4244 "decode: unknown leader recovery state on a known partition",
4245 change(0, |c| c.leader_recovery_state = 9),
4246 value("partition change leader recovery state", "unknown state 9"),
4247 ),
4248 (
4249 "decode: unknown fenced value",
4250 from_kraft(
4251 &KraftMetadataRecord::BrokerRegistrationChange(
4252 BrokerRegistrationChangeRecord {
4253 broker_id: 1,
4254 fenced: 7,
4255 ..Default::default()
4256 },
4257 ),
4258 &image,
4259 ),
4260 value("broker registration change fenced", "unknown value 7"),
4261 ),
4262 (
4263 "decode: negative node id",
4264 from_kraft(
4265 &KraftMetadataRecord::UnregisterBroker(KUnregisterBrokerRecord {
4266 broker_id: -1,
4267 ..Default::default()
4268 }),
4269 &image,
4270 ),
4271 value("unregister broker id", "-1 is negative"),
4272 ),
4273 (
4274 "encode: node id that exceeds i32",
4275 to_kraft(
4276 &MetadataRecord::V1ProducerIds(ProducerIdsRecord {
4277 broker_id: NodeId(1 << 31),
4278 broker_epoch: 0,
4279 next_producer_id: 0,
4280 }),
4281 &image,
4282 )
4283 .map(|_| unreachable!()),
4284 value("producer IDs broker id", "node_id 2147483648 exceeds i32"),
4285 ),
4286 ] {
4287 check!(got == want, "{case}");
4288 }
4289 }
4290
4291 #[test]
4294 fn byte_content_errors_are_invalid_values() {
4295 let image = img();
4296 let decode = |record: KraftMetadataRecord| from_kraft(&record, &image).map(drop);
4297 let encode = |record: MetadataRecord| to_kraft(&record, &image).map(drop);
4298 let scram = |mechanism: i8, iterations: i32| {
4299 decode(KraftMetadataRecord::UserScramCredential(
4300 UserScramCredentialRecord {
4301 name: "alice".into(),
4302 mechanism,
4303 iterations,
4304 ..Default::default()
4305 },
4306 ))
4307 };
4308 let token = |owner: &str, requester: &str, renewer: &str| {
4309 decode(KraftMetadataRecord::DelegationToken(
4310 KDelegationTokenRecord {
4311 owner: owner.into(),
4312 requester: requester.into(),
4313 renewers: vec![renewer.into()],
4314 ..Default::default()
4315 },
4316 ))
4317 };
4318 let config = |resource_type: i8, resource_name: &str| {
4319 decode(KraftMetadataRecord::Config(ConfigRecord {
4320 resource_type,
4321 resource_name: resource_name.into(),
4322 name: "k".into(),
4323 ..Default::default()
4324 }))
4325 };
4326 let value = |field, detail: &str| {
4327 Err(TranslateError::InvalidValue {
4328 field,
4329 detail: detail.into(),
4330 })
4331 };
4332
4333 for (case, got, want) in [
4334 (
4335 "a non-SCRAM mechanism on encode",
4336 scram_mechanism_to_wire(SaslMechanism::Plain).map(drop),
4337 value("scram mechanism", "Plain is not a SCRAM mechanism"),
4338 ),
4339 (
4340 "an unknown SCRAM mechanism byte",
4341 scram(9, 4096),
4342 value("scram mechanism", "unknown SCRAM mechanism wire byte 9"),
4343 ),
4344 (
4345 "negative SCRAM iterations",
4346 scram(1, -1),
4347 value("scram iterations", "-1 is negative"),
4348 ),
4349 (
4350 "SCRAM iterations that exceed i32 on encode",
4351 encode(MetadataRecord::V1ScramCredential(ScramCredentialRecord {
4352 user: "alice".into(),
4353 mechanism: SaslMechanism::ScramSha256,
4354 salt: vec![],
4355 stored_key: vec![],
4356 server_key: vec![],
4357 iterations: u32::MAX,
4358 })),
4359 value("scram iterations", "4294967295 exceeds i32"),
4360 ),
4361 (
4362 "an unknown ACL resource type",
4363 resource_type_from_wire(9).map(drop),
4364 value("acl resource_type", "unknown wire byte 9"),
4365 ),
4366 (
4367 "an unknown ACL pattern type",
4368 pattern_type_from_wire(9).map(drop),
4369 value("acl pattern_type", "unknown wire byte 9"),
4370 ),
4371 (
4372 "an unknown ACL operation",
4373 operation_from_wire(99).map(drop),
4374 value("acl operation", "unknown wire byte 99"),
4375 ),
4376 (
4377 "an unknown ACL permission type",
4378 permission_from_wire(9).map(drop),
4379 value("acl permission_type", "unknown wire byte 9"),
4380 ),
4381 (
4382 "an unknown security protocol",
4383 protocol_from_wire(9).map(drop),
4384 value("security_protocol", "unknown wire value 9"),
4385 ),
4386 (
4387 "an unregistered broker id that exceeds i32 on encode",
4388 encode(MetadataRecord::V1UnregisterBroker(UnregisterBrokerRecord {
4389 node_id: NodeId(1 << 31),
4390 broker_epoch: 0,
4391 })),
4392 value("broker_id", "node_id 2147483648 exceeds i32"),
4393 ),
4394 (
4395 "a registered broker id that exceeds i32 on encode",
4396 encode(MetadataRecord::V1BrokerRegistration(
4397 BrokerRegistrationRecord {
4398 node_id: NodeId(1 << 31),
4399 broker_epoch: 0,
4400 incarnation_id: uuid::Uuid::nil(),
4401 host: "localhost".into(),
4402 port: 9092,
4403 rack: None,
4404 log_dirs: vec![],
4405 fenced: false,
4406 in_controlled_shutdown: false,
4407 cordoned_log_dirs: None,
4408 endpoints: vec![],
4409 features: BTreeMap::new(),
4410 },
4411 )),
4412 value("broker_id", "node_id 2147483648 exceeds i32"),
4413 ),
4414 (
4415 "a registered controller id that exceeds i32 on encode",
4416 encode(MetadataRecord::V1ControllerRegistration(
4417 ControllerRegistrationRecord {
4418 node_id: NodeId(1 << 31),
4419 incarnation_id: uuid::Uuid::nil(),
4420 zk_migration_ready: false,
4421 endpoints: vec![],
4422 features: BTreeMap::new(),
4423 },
4424 )),
4425 value("controller_id", "node_id 2147483648 exceeds i32"),
4426 ),
4427 (
4428 "a delegation token owner that does not parse",
4429 token("alice", "User:bob", "User:carol"),
4430 value("delegation token owner", "invalid principal \"alice\""),
4431 ),
4432 (
4433 "a delegation token requester that does not parse",
4434 token("User:alice", "bob", "User:carol"),
4435 value("delegation token requester", "invalid principal \"bob\""),
4436 ),
4437 (
4438 "a delegation token renewer that does not parse",
4439 token("User:alice", "User:bob", "carol"),
4440 value("delegation token renewer", "invalid principal \"carol\""),
4441 ),
4442 (
4443 "a broker config resource name that is not a node id",
4444 config(4, "broker-one"),
4445 value(
4446 "broker config resource_name",
4447 "invalid digit found in string",
4448 ),
4449 ),
4450 (
4451 "an unsupported config resource type",
4452 config(99, "x"),
4453 value("config resource_type", "unsupported resource_type 99"),
4454 ),
4455 ] {
4456 check!(got == want, "{case}");
4457 }
4458 }
4459
4460 #[test]
4464 fn partition_encode_errors_split_into_reference_and_value() {
4465 let (image, _) = classification_image();
4466 let encode = |record: MetadataRecord| to_kraft(&record, &image).map(drop);
4467 let update = |partition: i32, edit: fn(&mut crate::PartitionUpdateRecord)| {
4468 let mut record = crate::PartitionUpdateRecord {
4469 partition: image.partition("t", 0).unwrap().clone(),
4470 eligible_leader_replicas: None,
4471 last_known_elr: None,
4472 recovery_state: None,
4473 };
4474 record.partition.partition = partition;
4475 edit(&mut record);
4476 encode(MetadataRecord::V1PartitionUpdate(record))
4477 };
4478 let reference = |field, detail: &str| {
4479 Err(TranslateError::InvalidReference {
4480 field,
4481 detail: detail.into(),
4482 })
4483 };
4484 let value = |field, detail: &str| {
4485 Err(TranslateError::InvalidValue {
4486 field,
4487 detail: detail.into(),
4488 })
4489 };
4490
4491 for (case, got, want) in [
4492 (
4493 "encode: directory assignment for a broker that is not a replica",
4494 encode(MetadataRecord::V1PartitionDirAssignment(
4495 PartitionDirAssignmentRecord {
4496 topic: "t".into(),
4497 partition: 0,
4498 replica: NodeId(2),
4499 directory: uuid::Uuid::from_u128(0xD1),
4500 },
4501 )),
4502 reference(
4503 "partition directory assignment",
4504 "broker 2 is not a replica of t-0",
4505 ),
4506 ),
4507 (
4508 "encode: partition update for an unknown partition",
4509 update(9, |_| {}),
4510 reference("partition update", "unknown partition t-9"),
4511 ),
4512 (
4513 "encode: partition update with a replica that exceeds i32",
4514 update(0, |u| u.partition.replicas = vec![NodeId(1 << 31)]),
4515 value("partition replicas", "node id 2147483648 exceeds i32"),
4516 ),
4517 (
4518 "encode: partition update with an eligible leader that exceeds i32",
4519 update(0, |u| {
4520 u.eligible_leader_replicas = Some(vec![NodeId(1 << 31)]);
4521 }),
4522 value("eligible leader replica", "node id 2147483648 exceeds i32"),
4523 ),
4524 (
4525 "encode: partition with a replica that exceeds i32",
4526 encode(MetadataRecord::V1Partition(PartitionRecord {
4527 replicas: vec![NodeId(1 << 31)],
4528 ..image.partition("t", 0).unwrap().clone()
4529 })),
4530 value("partition replicas", "node id 2147483648 exceeds i32"),
4531 ),
4532 (
4533 "encode: partition with a leader that exceeds i32",
4534 encode(MetadataRecord::V1Partition(PartitionRecord {
4535 leader: NodeId(1 << 31),
4536 ..image.partition("t", 0).unwrap().clone()
4537 })),
4538 value("partition leader", "leader 2147483648 exceeds i32"),
4539 ),
4540 ] {
4541 check!(got == want, "{case}");
4542 }
4543 }
4544}