1pub use self::{
6 client_software::is_valid_client_info,
7 features::{finalized_feature_keys, supported_feature_key, supported_feature_keys},
8};
9use {
10 self::table::CONTROLLER_LISTENER_APIS,
11 crate::{
12 ControllerAdminRouter, ControllerApiVersion, UnstableApiVersions, UnstableFeatureVersions,
13 error::RaftError, kafka_4_3_1_max,
14 },
15 bytes::{Bytes, BytesMut},
16 krabka_protocol::{
17 Decode, Encode,
18 owned::{
19 api_versions_request::{self, ApiVersionsRequest},
20 api_versions_response::{ApiVersion as ApiVersionEntry, ApiVersionsResponse},
21 },
22 },
23};
24
25mod client_software;
26mod features;
27pub(super) mod table;
28
29pub(super) const API_KEY_API_VERSIONS: i16 = 18;
33
34const API_VERSIONS_MIN_VERSION: i16 = api_versions_request::MIN_VERSION;
36const API_VERSIONS_MAX_VERSION: i16 = api_versions_request::MAX_VERSION;
39
40#[must_use]
45pub fn api_versions_max_version(unstable: UnstableApiVersions) -> i16 {
46 match unstable {
47 UnstableApiVersions::Enabled => API_VERSIONS_MAX_VERSION,
48 UnstableApiVersions::Disabled => {
49 kafka_4_3_1_max(API_KEY_API_VERSIONS).unwrap_or(API_VERSIONS_MAX_VERSION)
50 }
51 }
52}
53const API_VERSIONS_CLIENT_SOFTWARE_MIN_VERSION: i16 = 3;
56const API_VERSIONS_ROUTING_MIN_VERSION: i16 = 5;
59const API_VERSIONS_UNSUPPORTED_VERSION: i16 = 35;
60const API_VERSIONS_INVALID_REQUEST: i16 = 42;
61
62#[derive(Clone, Copy)]
64pub struct ApiVersionsView<'a> {
65 pub(crate) image: &'a krabka_metadata::MetadataImage,
67 pub(crate) finalized_features_epoch: i64,
73 pub(crate) admin_router: Option<&'a dyn ControllerAdminRouter>,
75 pub(crate) unstable: UnstableApiVersions,
80}
81
82pub fn api_versions_response(
108 req_version: i16,
109 body: &[u8],
110 view: ApiVersionsView<'_>,
111) -> Result<Bytes, RaftError> {
112 if !(API_VERSIONS_MIN_VERSION..=api_versions_max_version(view.unstable)).contains(&req_version)
113 {
114 return Ok(encode_body(&unsupported_version_response(view.unstable), 0));
115 }
116 let request = ApiVersionsRequest::decode(&mut &body[..], req_version)?;
117 if !is_valid_api_versions_request(&request, req_version) {
118 return Ok(encode_body(
119 &ApiVersionsResponse {
120 error_code: API_VERSIONS_INVALID_REQUEST,
121 ..Default::default()
122 },
123 req_version,
124 ));
125 }
126 Ok(api_versions_response_body(req_version, view))
127}
128
129#[must_use]
138pub fn unsupported_version_response(unstable: UnstableApiVersions) -> ApiVersionsResponse {
139 ApiVersionsResponse {
140 error_code: API_VERSIONS_UNSUPPORTED_VERSION,
141 api_keys: vec![ApiVersionEntry {
142 api_key: API_KEY_API_VERSIONS,
143 min_version: API_VERSIONS_MIN_VERSION,
144 max_version: api_versions_max_version(unstable),
145 ..Default::default()
146 }],
147 ..Default::default()
148 }
149}
150
151#[must_use]
153pub fn is_valid_api_versions_request(request: &ApiVersionsRequest, version: i16) -> bool {
154 if version >= API_VERSIONS_ROUTING_MIN_VERSION
155 && (request.cluster_id.is_none() != (request.node_id == -1))
156 {
157 return false;
158 }
159 version < API_VERSIONS_CLIENT_SOFTWARE_MIN_VERSION
160 || (is_valid_client_info(&request.client_software_name)
161 && is_valid_client_info(&request.client_software_version))
162}
163
164fn encode_body(response: &ApiVersionsResponse, version: i16) -> Bytes {
165 let mut body = BytesMut::with_capacity(response.encoded_len(version));
166 let _ = response.encode(&mut body, version);
169 body.freeze()
170}
171
172pub(super) fn advertised_api_keys(
178 admin_router: Option<&dyn ControllerAdminRouter>,
179 unstable: UnstableApiVersions,
180) -> Vec<ApiVersionEntry> {
181 let entry = |version: &ControllerApiVersion| {
182 version
183 .enabled_max(unstable)
184 .map(|max_version| ApiVersionEntry {
185 api_key: version.api_key,
186 min_version: version.min_version,
187 max_version,
188 ..Default::default()
189 })
190 };
191 let mut api_keys: Vec<ApiVersionEntry> =
192 CONTROLLER_LISTENER_APIS.iter().filter_map(entry).collect();
193 if let Some(router) = admin_router {
194 api_keys.extend(router.api_versions().iter().filter_map(entry));
195 }
196 api_keys.sort_unstable_by_key(|version| version.api_key);
197 api_keys
198}
199
200pub(super) fn is_disabled_version(
206 api_key: i16,
207 version: i16,
208 admin_router: Option<&dyn ControllerAdminRouter>,
209 unstable: UnstableApiVersions,
210) -> bool {
211 let router_apis = admin_router.map_or(&[][..], |router| router.api_versions());
212 CONTROLLER_LISTENER_APIS
213 .iter()
214 .chain(router_apis)
215 .find(|api| api.api_key == api_key)
216 .is_some_and(|api| api.is_disabled_version(version, unstable))
217}
218
219pub(super) fn api_versions_response_body(req_version: i16, view: ApiVersionsView<'_>) -> Bytes {
246 let resp = ApiVersionsResponse {
247 api_keys: advertised_api_keys(view.admin_router, view.unstable),
248 supported_features: supported_feature_keys(
249 req_version,
250 UnstableFeatureVersions::from(view.unstable == UnstableApiVersions::Enabled),
251 ),
252 finalized_features_epoch: view.finalized_features_epoch,
253 finalized_features: finalized_feature_keys(view.image),
254 ..Default::default()
255 };
256 encode_body(
262 &resp,
263 req_version.clamp(0, api_versions_max_version(view.unstable)),
264 )
265}
266
267#[cfg(test)]
269pub(super) const BROKER_HEARTBEAT_ONLY: &[ControllerApiVersion] = {
270 use krabka_protocol::owned::broker_heartbeat_request;
271 &[ControllerApiVersion {
272 api_key: broker_heartbeat_request::API_KEY,
273 min_version: broker_heartbeat_request::MIN_VERSION,
274 max_version: broker_heartbeat_request::MAX_VERSION,
275 released_max: Some(broker_heartbeat_request::MAX_VERSION),
276 flexible_min: broker_heartbeat_request::FLEXIBLE_MIN,
277 }]
278};
279
280#[cfg(test)]
281mod tests {
282 use {
283 super::*,
284 crate::{
285 ControllerAdminRequest, ControllerAdminRouteFuture, ControllerAdminRouter,
286 ControllerApiVersion, LATEST_PRODUCTION_METADATA_VERSION, test_support::feature_image,
287 },
288 krabka_metadata::{
289 FeatureLevelRecord, MetadataRecord,
290 metadata_version::{
291 METADATA_VERSION_FEATURE, METADATA_VERSION_MAX, METADATA_VERSION_MIN,
292 },
293 },
294 krabka_protocol::{
295 Decode,
296 owned::broker_heartbeat_request::{MAX_VERSION, MIN_VERSION},
297 },
298 std::collections::BTreeSet,
299 uuid::Uuid,
300 };
301
302 fn view(
306 image: &krabka_metadata::MetadataImage,
307 finalized_features_epoch: i64,
308 ) -> ApiVersionsView<'_> {
309 ApiVersionsView {
310 image,
311 finalized_features_epoch,
312 admin_router: None,
313 unstable: UnstableApiVersions::Disabled,
314 }
315 }
316
317 #[yare::parameterized(
318 legacy_shape = { 0i16 },
319 flexible_shape = { 4i16 },
320 )]
321 fn api_versions_body_advertises_kip595_set_both_shapes(req_v: i16) {
322 use krabka_protocol::{Decode, owned::api_versions_response::ApiVersionsResponse};
323 let mut image = krabka_metadata::MetadataImage::new(Uuid::nil());
324 image.apply(&MetadataRecord::V1FeatureLevel(FeatureLevelRecord {
325 name: METADATA_VERSION_FEATURE.into(),
326 level: 24,
327 }));
328 let body = super::api_versions_response_body(req_v, view(&image, 41));
329 let v = req_v.clamp(0, 4);
330 let mut cur = &body[..];
331 let resp = ApiVersionsResponse::decode(&mut cur, v).expect("decode body");
332 assert2::assert!(cur.is_empty());
333 assert2::assert!(resp.error_code == 0);
334 let keys: BTreeSet<i16> = resp.api_keys.iter().map(|k| k.api_key).collect();
335 for want in [1i16, 18, 52, 53, 54, 59, 70] {
336 assert2::assert!(keys.contains(&want));
337 }
338 assert2::assert!(!keys.contains(&62i16));
344 assert2::assert!(!keys.contains(&63i16));
345 let vote = resp.api_keys.iter().find(|k| k.api_key == 52).unwrap();
348 assert2::assert!(vote.min_version == 0 && vote.max_version == 2);
349 if req_v >= 3 {
350 let finalized_metadata = resp
351 .finalized_features
352 .iter()
353 .find(|feature| feature.name == "metadata.version")
354 .expect("metadata.version finalized");
355 assert2::assert!(
356 (
357 finalized_metadata.min_version_level,
358 finalized_metadata.max_version_level
359 ) == (24, 24)
360 );
361 assert2::assert!(resp.finalized_features_epoch == 41);
362 }
363 }
364
365 #[test]
369 fn an_admin_router_contributes_its_own_api_versions() {
370 use krabka_protocol::owned::api_versions_response::ApiVersionsResponse;
371
372 struct HeartbeatRouter;
373 impl ControllerAdminRouter for HeartbeatRouter {
374 fn api_versions(&self) -> &[ControllerApiVersion] { super::BROKER_HEARTBEAT_ONLY }
375
376 fn route(&self, _request: ControllerAdminRequest) -> ControllerAdminRouteFuture<'_> {
377 Box::pin(async { Ok(None) })
378 }
379 }
380
381 let image = krabka_metadata::MetadataImage::new(Uuid::nil());
382 let body = super::api_versions_response_body(4, ApiVersionsView {
383 admin_router: Some(&HeartbeatRouter),
384 ..view(&image, -1)
385 });
386 let resp = ApiVersionsResponse::decode(&mut &body[..], 4).expect("decode body");
387
388 let heartbeat = resp
389 .api_keys
390 .iter()
391 .find(|key| key.api_key == 63)
392 .expect("BrokerHeartbeat advertised through the Admin router");
393 assert2::assert!(
394 (heartbeat.min_version, heartbeat.max_version) == (MIN_VERSION, MAX_VERSION,)
395 );
396 }
397
398 struct UnstableRouter;
401
402 impl ControllerAdminRouter for UnstableRouter {
403 fn api_versions(&self) -> &[ControllerApiVersion] {
404 &[ControllerApiVersion {
405 api_key: 22,
406 min_version: 0,
407 max_version: 6,
408 released_max: Some(5),
409 flexible_min: 2,
410 }]
411 }
412
413 fn route(&self, _request: ControllerAdminRequest) -> ControllerAdminRouteFuture<'_> {
414 Box::pin(async { Ok(None) })
415 }
416 }
417
418 #[yare::parameterized(
422 disabled = { UnstableApiVersions::Disabled, 5, true },
423 enabled = { UnstableApiVersions::Enabled, 6, false },
424 )]
425 fn unstable_last_versions_follow_unstable_api_versions_enable(
426 unstable: UnstableApiVersions,
427 advertised_max: i16,
428 v6_disabled: bool,
429 ) {
430 let api_keys = advertised_api_keys(Some(&UnstableRouter), unstable);
431 let row = api_keys
432 .iter()
433 .find(|key| key.api_key == 22)
434 .expect("router api advertised");
435 assert2::check!(
436 (row.min_version, row.max_version) == (0, advertised_max),
437 "{unstable:?}"
438 );
439 assert2::check!(
440 is_disabled_version(22, 6, Some(&UnstableRouter), unstable) == v6_disabled,
441 "{unstable:?}"
442 );
443 assert2::check!(
444 !is_disabled_version(22, 5, Some(&UnstableRouter), unstable),
445 "{unstable:?}"
446 );
447 assert2::check!(!is_disabled_version(22, 7, Some(&UnstableRouter), unstable));
450 assert2::check!(!is_disabled_version(22, 6, None, unstable));
451 }
452
453 #[yare::parameterized(
457 disabled = { UnstableApiVersions::Disabled, None, true },
458 enabled = { UnstableApiVersions::Enabled, Some((0, 0)), false },
459 )]
460 fn an_api_kafka_4_3_1_lacks_follows_unstable_api_versions_enable(
461 unstable: UnstableApiVersions,
462 advertised: Option<(i16, i16)>,
463 disabled: bool,
464 ) {
465 struct TrunkRouter;
466 impl ControllerAdminRouter for TrunkRouter {
467 fn api_versions(&self) -> &[ControllerApiVersion] {
468 &[ControllerApiVersion {
469 api_key: 94,
470 min_version: 0,
471 max_version: 0,
472 released_max: None,
473 flexible_min: 0,
474 }]
475 }
476
477 fn route(&self, _request: ControllerAdminRequest) -> ControllerAdminRouteFuture<'_> {
478 Box::pin(async { Ok(None) })
479 }
480 }
481
482 let row = advertised_api_keys(Some(&TrunkRouter), unstable)
483 .into_iter()
484 .find(|key| key.api_key == 94)
485 .map(|key| (key.min_version, key.max_version));
486 assert2::check!(row == advertised, "{unstable:?}");
487 assert2::check!(
488 is_disabled_version(94, 0, Some(&TrunkRouter), unstable) == disabled,
489 "{unstable:?}"
490 );
491 }
492
493 #[test]
497 fn the_advertised_table_is_strictly_ascending() {
498 let api_keys = advertised_api_keys(Some(&UnstableRouter), UnstableApiVersions::Disabled);
499 assert2::assert!(
500 api_keys
501 .windows(2)
502 .all(|pair| pair[0].api_key < pair[1].api_key)
503 );
504 }
505
506 #[test]
511 fn api_versions_response_runs_kafka_request_checks() {
512 use krabka_protocol::owned::api_versions_request::ApiVersionsRequest;
513
514 struct Answer {
516 version: i16,
517 response: ApiVersionsResponse,
518 }
519 struct Row {
520 label: &'static str,
521 unstable: UnstableApiVersions,
522 version: i16,
523 request: Option<ApiVersionsRequest>,
524 expected: Option<Answer>,
525 }
526 let request = |name: &str, software_version: &str, cluster_id: Option<&str>, node_id| {
527 Some(ApiVersionsRequest {
528 client_software_name: name.into(),
529 client_software_version: software_version.into(),
530 cluster_id: cluster_id.map(str::to_owned),
531 node_id,
532 ..Default::default()
533 })
534 };
535 let unsupported = |max_version| {
536 Some(Answer {
537 version: 0,
538 response: ApiVersionsResponse {
539 error_code: API_VERSIONS_UNSUPPORTED_VERSION,
540 api_keys: vec![ApiVersionEntry {
541 api_key: 18,
542 min_version: 0,
543 max_version,
544 ..Default::default()
545 }],
546 ..Default::default()
547 },
548 })
549 };
550 let (strict, trunk) = (UnstableApiVersions::Disabled, UnstableApiVersions::Enabled);
551 let invalid = |version| {
552 Some(Answer {
553 version,
554 response: ApiVersionsResponse {
555 error_code: API_VERSIONS_INVALID_REQUEST,
556 ..Default::default()
557 },
558 })
559 };
560 let rows = [
561 Row {
562 label: "v6",
563 unstable: strict,
564 version: 6,
565 request: None,
566 expected: unsupported(4),
567 },
568 Row {
569 label: "i16::MAX",
570 unstable: strict,
571 version: i16::MAX,
572 request: None,
573 expected: unsupported(4),
574 },
575 Row {
576 label: "negative",
577 unstable: strict,
578 version: -1,
579 request: None,
580 expected: unsupported(4),
581 },
582 Row {
583 label: "v3 empty name",
584 unstable: strict,
585 version: 3,
586 request: request("", "1.0", None, -1),
587 expected: invalid(3),
588 },
589 Row {
590 label: "v4 name with a space",
591 unstable: strict,
592 version: 4,
593 request: request("a b", "1.0", None, -1),
594 expected: invalid(4),
595 },
596 Row {
597 label: "v3 empty software version",
598 unstable: strict,
599 version: 3,
600 request: request("krabka", "", None, -1),
601 expected: invalid(3),
602 },
603 Row {
604 label: "v5 cluster id without node id",
605 unstable: trunk,
606 version: 5,
607 request: request("krabka", "1.0", Some("cluster"), -1),
608 expected: invalid(5),
609 },
610 Row {
611 label: "v5 node id without cluster id",
612 unstable: trunk,
613 version: 5,
614 request: request("krabka", "1.0", None, 7),
615 expected: invalid(5),
616 },
617 Row {
618 label: "v5 another cluster and node, no KIP-1242 check",
619 unstable: trunk,
620 version: 5,
621 request: request("krabka", "1.0", Some("other"), 8),
622 expected: None,
623 },
624 Row {
625 label: "v5 valid",
626 unstable: trunk,
627 version: 5,
628 request: request("krabka", "1.0", None, -1),
629 expected: None,
630 },
631 Row {
632 label: "v0",
633 unstable: strict,
634 version: 0,
635 request: Some(ApiVersionsRequest::default()),
636 expected: None,
637 },
638 Row {
639 label: "v6 under unstable api versions",
640 unstable: trunk,
641 version: 6,
642 request: None,
643 expected: unsupported(5),
644 },
645 Row {
646 label: "v5 is Kafka 4.3.1's unsupported version",
647 unstable: strict,
648 version: 5,
649 request: request("krabka", "1.0", None, -1),
650 expected: unsupported(4),
651 },
652 ];
653
654 let image = krabka_metadata::MetadataImage::new(Uuid::nil());
655 for row in rows {
656 let body = row.request.map_or_else(
657 || bytes::Bytes::from_static(&[0xff]),
658 |request| {
659 let mut body = BytesMut::new();
660 request.encode(&mut body, row.version).expect("encode");
661 body.freeze()
662 },
663 );
664 let view = ApiVersionsView {
665 unstable: row.unstable,
666 ..view(&image, -1)
667 };
668 let answer = super::api_versions_response(row.version, &body, view).expect("answer");
669 let Answer {
670 version,
671 response: expected,
672 } = row.expected.unwrap_or_else(|| {
673 let full = super::api_versions_response_body(row.version, view);
674 Answer {
675 version: row.version,
676 response: ApiVersionsResponse::decode(&mut &full[..], row.version)
677 .expect("full"),
678 }
679 });
680 let mut cur = &answer[..];
681 let decoded = ApiVersionsResponse::decode(&mut cur, version).expect("decode");
682 assert2::check!(cur.is_empty(), "{}", row.label);
683 assert2::check!(decoded == expected, "{}", row.label);
684 if expected.error_code == 0 {
685 assert2::check!(!decoded.api_keys.is_empty(), "{}", row.label);
686 }
687 }
688 }
689
690 #[test]
693 fn api_versions_response_refuses_a_malformed_body() {
694 let image = krabka_metadata::MetadataImage::new(Uuid::nil());
695 assert2::assert!(super::api_versions_response(3, &[0xff], view(&image, -1)).is_err());
696 }
697
698 #[test]
703 fn feature_fields_match_kafka_on_the_controller_listener() {
704 use krabka_protocol::owned::api_versions_response::{
705 FinalizedFeatureKey, SupportedFeatureKey,
706 };
707
708 struct Row<'a> {
709 version: i16,
710 kraft_version: u16,
711 levels: &'a [(&'a str, i16)],
712 want_supported: &'a Vec<SupportedFeatureKey>,
713 want_finalized: Vec<FinalizedFeatureKey>,
714 }
715
716 krabka_macros::supported_feature_fixture!(supported);
717 krabka_macros::supported_features_fixture!(modern_supported, supported);
718 krabka_macros::finalized_feature_fixture!(finalized);
719 let metadata_max = LATEST_PRODUCTION_METADATA_VERSION;
722 let legacy = vec![supported(
723 "metadata.version",
724 METADATA_VERSION_MIN,
725 metadata_max,
726 )];
727 let modern = modern_supported(
728 supported("metadata.version", METADATA_VERSION_MIN, metadata_max),
729 1,
730 );
731 let rows = [
732 Row {
733 version: 3,
734 kraft_version: 0,
735 levels: &[("metadata.version", 25)],
736 want_supported: &legacy,
737 want_finalized: vec![finalized("metadata.version", 25)],
738 },
739 Row {
740 version: 4,
741 kraft_version: 0,
742 levels: &[("metadata.version", 25), ("group.version", 1)],
743 want_supported: &modern,
744 want_finalized: vec![
745 finalized("group.version", 1),
746 finalized("metadata.version", 25),
747 ],
748 },
749 Row {
750 version: 4,
751 kraft_version: 1,
752 levels: &[("metadata.version", 25)],
753 want_supported: &modern,
754 want_finalized: vec![
755 finalized("kraft.version", 1),
756 finalized("metadata.version", 25),
757 ],
758 },
759 Row {
760 version: 4,
761 kraft_version: 1,
762 levels: &[("metadata.version", 30), ("krabka.version", 1)],
763 want_supported: &modern,
764 want_finalized: vec![
765 finalized("krabka.version", 1),
766 finalized("kraft.version", 1),
767 finalized("metadata.version", 30),
768 ],
769 },
770 ];
771 for Row {
772 version,
773 kraft_version,
774 levels,
775 want_supported,
776 want_finalized,
777 } in rows
778 {
779 let image = feature_image(kraft_version, levels);
780 let body = super::api_versions_response_body(version, view(&image, 1234));
781 let resp = ApiVersionsResponse::decode(&mut &body[..], version).expect("decode");
782 assert2::check!(
783 (
784 &resp.supported_features,
785 &resp.finalized_features,
786 resp.finalized_features_epoch
787 ) == (want_supported, &want_finalized, 1234),
788 "v{version}, kraft.version {kraft_version}"
789 );
790 }
791 }
792
793 #[yare::parameterized(
799 kraft_version_0 = { 0, &[("metadata.version", METADATA_VERSION_MIN)] },
800 kraft_version_1 = {
801 1,
802 &[("kraft.version", 1), ("metadata.version", METADATA_VERSION_MIN)],
803 },
804 )]
805 fn a_fresh_controller_advertises_kafkas_minimum_metadata_version(
806 kraft_version: u16,
807 want: &[(&str, i16)],
808 ) {
809 krabka_macros::finalized_feature_fixture!(finalized);
810 let image = feature_image(kraft_version, &[]);
811 let body = super::api_versions_response_body(4, view(&image, -1));
812 let resp = ApiVersionsResponse::decode(&mut &body[..], 4).expect("decode");
813 let want_finalized: Vec<_> = want
814 .iter()
815 .map(|(name, level)| finalized(name, *level))
816 .collect();
817 assert2::check!(
818 (resp.finalized_features, resp.finalized_features_epoch) == (want_finalized, -1)
819 );
820 }
821
822 #[yare::parameterized(
830 disabled = { UnstableApiVersions::Disabled, LATEST_PRODUCTION_METADATA_VERSION },
831 enabled = {
832 UnstableApiVersions::Enabled,
833 METADATA_VERSION_MAX,
834 },
835 )]
836 fn the_controller_listener_caps_supported_features_with_the_api_flag(
837 unstable: UnstableApiVersions,
838 want_max: i16,
839 ) {
840 let image = krabka_metadata::MetadataImage::new(Uuid::nil());
841 let view = ApiVersionsView {
842 unstable,
843 ..view(&image, 0)
844 };
845 let body = super::api_versions_response_body(4, view);
846 let resp = ApiVersionsResponse::decode(&mut &body[..], 4).expect("decode");
847 let metadata_version = resp
848 .supported_features
849 .iter()
850 .find(|feature| feature.name == "metadata.version")
851 .expect("metadata.version is supported");
852 assert2::check!(
853 metadata_version.max_version == want_max,
854 "unstable.api.versions.enable {unstable:?}"
855 );
856 }
857}