1use std::collections::BTreeSet;
5
6use krabka_protocol::owned::{
7 describe_cluster_request::DescribeClusterRequest,
8 describe_cluster_response::{DescribeClusterBroker, DescribeClusterResponse},
9};
10
11use crate::{
12 AdminClient, AdminError, kafka_error_name,
13 retry::ControllerRetry,
14 users::{AclOperation, wire_to_operation},
15};
16
17const ENDPOINT_TYPE_BROKERS: i8 = 1;
19const ENDPOINT_TYPE_CONTROLLERS: i8 = 2;
22const AUTHORIZED_OPERATIONS_OMITTED: i32 = i32::MIN;
25
26#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, bon::Builder)]
29pub struct DescribeClusterOptions {
30 #[builder(default)]
32 pub include_authorized_operations: bool,
33 #[builder(default)]
35 pub include_fenced_brokers: bool,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct ClusterNode {
41 pub id: i32,
43 pub host: String,
45 pub port: i32,
47 pub rack: Option<String>,
49 pub is_fenced: bool,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct ClusterDescription {
57 pub cluster_id: String,
59 pub controller: Option<ClusterNode>,
63 pub nodes: Vec<ClusterNode>,
65 pub authorized_operations: Option<BTreeSet<AclOperation>>,
68}
69
70fn cluster_node(broker: DescribeClusterBroker) -> ClusterNode {
71 ClusterNode {
72 id: broker.broker_id,
73 host: broker.host,
74 port: broker.port,
75 rack: broker.rack,
76 is_fenced: broker.is_fenced,
77 }
78}
79
80pub(crate) fn node_with_host_port(id: i32, host: String, port: i32) -> ClusterNode {
81 ClusterNode {
82 id,
83 host,
84 port,
85 rack: None,
86 is_fenced: false,
87 }
88}
89
90pub(crate) fn authorized_operations(bits: i32) -> Option<BTreeSet<AclOperation>> {
94 if bits == AUTHORIZED_OPERATIONS_OMITTED {
95 return None;
96 }
97 Some(
98 (0_i8..32)
99 .filter(|bit| bits & (1_i32 << bit) != 0)
100 .filter_map(|bit| wire_to_operation(bit).ok())
101 .filter(|operation| *operation != AclOperation::All)
102 .collect(),
103 )
104}
105
106fn cluster_description(
109 response: DescribeClusterResponse,
110) -> Result<ClusterDescription, AdminError> {
111 if response.error_code != 0 {
112 return Err(AdminError::Broker {
113 api: "DescribeCluster",
114 code: response.error_code,
115 name: kafka_error_name(response.error_code),
116 message: response.error_message,
117 });
118 }
119 let nodes = response
120 .brokers
121 .into_iter()
122 .map(cluster_node)
123 .collect::<Vec<_>>();
124 let controller = nodes
125 .iter()
126 .find(|node| node.id == response.controller_id)
127 .cloned();
128 Ok(ClusterDescription {
129 cluster_id: response.cluster_id,
130 controller,
131 nodes,
132 authorized_operations: authorized_operations(response.cluster_authorized_operations),
133 })
134}
135
136impl AdminClient {
137 pub async fn describe_cluster(
156 &self,
157 options: DescribeClusterOptions,
158 ) -> Result<ClusterDescription, AdminError> {
159 let controllers = self.conn.uses_controller_bootstrap();
160 let request = DescribeClusterRequest {
161 include_cluster_authorized_operations: options.include_authorized_operations,
162 endpoint_type: if controllers {
163 ENDPOINT_TYPE_CONTROLLERS
164 } else {
165 ENDPOINT_TYPE_BROKERS
166 },
167 include_fenced_brokers: options.include_fenced_brokers,
168 ..Default::default()
169 };
170 let min_version = if options.include_fenced_brokers {
171 2
172 } else {
173 i16::from(controllers)
174 };
175 let retry = ControllerRetry::new("DescribeCluster", self.retry);
176 let response = retry
177 .bounded(self.conn.send_at_least(request, min_version))
178 .await?;
179 cluster_description(response)
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use std::sync::{Arc, Mutex};
186
187 use krabka_client_core::MockBroker;
188 use krabka_protocol::owned::{api_versions_request, describe_cluster_request};
189
190 use super::*;
191 use crate::partition_leaders::test_support::{
192 ApiVersionRange, api_versions, decode_request, encode_response, fast_admin,
193 };
194
195 #[test]
196 fn authorized_operations_skip_all_and_unknown_bits() {
197 let bits = 1 | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 13) | (1 << 20);
200 for (name, bits, expected) in [
201 ("omitted", i32::MIN, None),
202 ("none", 0, Some(BTreeSet::new())),
203 (
204 "some",
205 bits,
206 Some(maplit::btreeset! {
207 AclOperation::Read,
208 AclOperation::CreateTokens,
209 }),
210 ),
211 ] {
212 assert2::assert!(authorized_operations(bits) == expected, "case {name}");
213 }
214 }
215
216 fn broker(broker_id: i32, rack: Option<&str>, is_fenced: bool) -> DescribeClusterBroker {
217 DescribeClusterBroker {
218 broker_id,
219 host: format!("broker-{broker_id}"),
220 port: 9092,
221 rack: rack.map(str::to_owned),
222 is_fenced,
223 ..Default::default()
224 }
225 }
226
227 fn node(id: i32, rack: Option<&str>, is_fenced: bool) -> ClusterNode {
228 ClusterNode {
229 id,
230 host: format!("broker-{id}"),
231 port: 9092,
232 rack: rack.map(str::to_owned),
233 is_fenced,
234 }
235 }
236
237 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
241 async fn describe_cluster_maps_the_answer_as_kafka_does() {
242 let answer =
243 |error_code, controller_id, cluster_authorized_operations| DescribeClusterResponse {
244 error_code,
245 error_message: (error_code != 0).then(|| "denied".to_owned()),
246 cluster_id: "lkc-1".to_owned(),
247 controller_id,
248 brokers: vec![broker(1, Some("r1"), false), broker(2, None, true)],
249 cluster_authorized_operations,
250 ..Default::default()
251 };
252 let nodes = vec![node(1, Some("r1"), false), node(2, None, true)];
253 for (name, options, response, expected) in [
254 (
255 "plain",
256 DescribeClusterOptions::builder().build(),
257 answer(0, 2, i32::MIN),
258 Ok(ClusterDescription {
259 cluster_id: "lkc-1".to_owned(),
260 controller: Some(node(2, None, true)),
261 nodes: nodes.clone(),
262 authorized_operations: None,
263 }),
264 ),
265 (
266 "fenced brokers and authorized operations",
267 DescribeClusterOptions::builder()
268 .include_authorized_operations(true)
269 .include_fenced_brokers(true)
270 .build(),
271 answer(0, 7, 1 << 8),
272 Ok(ClusterDescription {
273 cluster_id: "lkc-1".to_owned(),
274 controller: None,
275 nodes,
276 authorized_operations: Some(maplit::btreeset! {
277 AclOperation::Describe,
278 }),
279 }),
280 ),
281 (
282 "cluster authorization failed is final",
283 DescribeClusterOptions::builder().build(),
284 answer(31, 1, i32::MIN),
285 Err(("DescribeCluster", 31, Some("denied".to_owned()))),
286 ),
287 ] {
288 let requests = Arc::new(Mutex::new(Vec::<DescribeClusterRequest>::new()));
289 let handler_requests = Arc::clone(&requests);
290 let broker = MockBroker::start(move |api_key, version, _, body| match api_key {
291 api_versions_request::API_KEY => Some(
292 api_versions()
293 .apis(vec![
294 ApiVersionRange::builder()
295 .api_key(describe_cluster_request::API_KEY)
296 .max_version(2)
297 .build(),
298 ])
299 .call(),
300 ),
301 describe_cluster_request::API_KEY => {
302 handler_requests
303 .lock()
304 .expect("requests lock")
305 .push(decode_request(body, version, true));
306 Some(encode_response(&response, version, true))
307 }
308 _ => None,
309 })
310 .await
311 .unwrap();
312 let admin = fast_admin(broker.addr, krabka_units::secs(5)).await;
313
314 let result = admin
315 .describe_cluster(options)
316 .await
317 .map_err(|error| match error {
318 AdminError::Broker {
319 api, code, message, ..
320 } => (api, code, message),
321 other => panic!("case {name}: unexpected error {other:?}"),
322 });
323
324 broker.stop();
325 assert2::assert!(
326 (result, requests.lock().expect("requests lock").clone())
327 == (
328 expected,
329 vec![DescribeClusterRequest {
330 include_cluster_authorized_operations: options
331 .include_authorized_operations,
332 endpoint_type: ENDPOINT_TYPE_BROKERS,
333 include_fenced_brokers: options.include_fenced_brokers,
334 ..Default::default()
335 }]
336 ),
337 "case {name}"
338 );
339 }
340 }
341
342 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
345 async fn fenced_brokers_need_describe_cluster_v2() {
346 let broker = MockBroker::start(move |api_key, _, _, _| match api_key {
347 api_versions_request::API_KEY => Some(
348 api_versions()
349 .apis(vec![
350 ApiVersionRange::builder()
351 .api_key(describe_cluster_request::API_KEY)
352 .max_version(1)
353 .build(),
354 ])
355 .call(),
356 ),
357 _ => None,
358 })
359 .await
360 .unwrap();
361 let admin = fast_admin(broker.addr, krabka_units::secs(5)).await;
362
363 let result = admin
364 .describe_cluster(
365 DescribeClusterOptions::builder()
366 .include_fenced_brokers(true)
367 .build(),
368 )
369 .await;
370
371 broker.stop();
372 assert2::assert!(matches!(
373 result,
374 Err(AdminError::Transport(
375 krabka_client_core::ClientError::IncompatibleVersion { .. }
376 ))
377 ));
378 }
379}