Skip to main content

krabka_client_admin/
cluster.rs

1//! Cluster description (`DescribeCluster`, KIP-700), as Kafka's
2//! `Admin.describeCluster`.
3
4use 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
17/// `endpointType` of a `DescribeCluster` request for broker endpoints.
18const ENDPOINT_TYPE_BROKERS: i8 = 1;
19/// `endpointType` of a `DescribeCluster` request for controller endpoints
20/// (KIP-919).
21const ENDPOINT_TYPE_CONTROLLERS: i8 = 2;
22/// The authorized-operations value of a response that did not compute them
23/// (`Integer.MIN_VALUE`).
24const AUTHORIZED_OPERATIONS_OMITTED: i32 = i32::MIN;
25
26/// What [`AdminClient::describe_cluster`] asks for, as Kafka's
27/// `DescribeClusterOptions`.
28#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, bon::Builder)]
29pub struct DescribeClusterOptions {
30    /// Ask for the operations that the caller may perform on the cluster.
31    #[builder(default)]
32    pub include_authorized_operations: bool,
33    /// List fenced brokers too (KIP-1073). Needs `DescribeCluster` v2.
34    #[builder(default)]
35    pub include_fenced_brokers: bool,
36}
37
38/// One node of the cluster, as Kafka's `Node`.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct ClusterNode {
41    /// The broker or controller ID.
42    pub id: i32,
43    /// The advertised host.
44    pub host: String,
45    /// The advertised port.
46    pub port: i32,
47    /// The rack, when the node has one.
48    pub rack: Option<String>,
49    /// Whether the controller has fenced this broker (KIP-1073).
50    pub is_fenced: bool,
51}
52
53/// The cluster that [`AdminClient::describe_cluster`] found, as Kafka's
54/// `DescribeClusterResult`.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct ClusterDescription {
57    /// The cluster ID.
58    pub cluster_id: String,
59    /// The node that the response names as controller, or `None` when it
60    /// names no listed node. A broker names a random live broker, as `KRaft`
61    /// clients cannot reach the active controller directly.
62    pub controller: Option<ClusterNode>,
63    /// Every node of the response.
64    pub nodes: Vec<ClusterNode>,
65    /// The operations that the caller may perform on the cluster, or `None`
66    /// when the caller did not ask for them.
67    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
90/// The operations of an authorized-operations bit field, as Kafka's
91/// `KafkaAdminClient.validAclOperations` reads it: `None` for
92/// `Integer.MIN_VALUE`, and without `UNKNOWN`, `ANY` and `ALL`.
93pub(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
106/// Maps a `DescribeCluster` answer to the cluster description, as Kafka's
107/// `describeCluster` `handleResponse` does.
108fn 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    /// Describes the cluster: its ID, its nodes, the node that serves as
138    /// controller, and optionally the operations that the caller may perform
139    /// on it, as Kafka's `describeCluster` operation does.
140    ///
141    /// The client sends `DescribeCluster` on its connection. Through broker
142    /// bootstrap it asks for broker endpoints; through controller bootstrap
143    /// (KIP-919) it asks for controller endpoints with v1 or higher, as
144    /// Kafka's `KafkaAdminClient` does. `include_fenced_brokers` needs v2,
145    /// as Kafka's builder refuses a lower version. The call stops at Kafka's
146    /// default `default.api.timeout.ms` (60 s).
147    ///
148    /// # Errors
149    ///
150    /// Returns [`AdminError::Broker`] for a top-level error, such as
151    /// `CLUSTER_AUTHORIZATION_FAILED` (31), and with `REQUEST_TIMED_OUT` (7)
152    /// at the deadline. Returns [`AdminError::Transport`] when the
153    /// connection fails, or when the peer does not support the
154    /// `DescribeCluster` version that the options need.
155    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        // Bits 0 (UNKNOWN), 1 (ANY), 2 (ALL), 3 (READ), 13 (CREATE_TOKENS)
198        // and 20 (no such operation).
199        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    /// Kafka's `describeCluster` asks for broker endpoints, names the
238    /// listed node with the controller ID as controller, reads the
239    /// authorized operations, and fails on a top-level code.
240    #[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    /// Kafka's `DescribeClusterRequest.Builder` refuses
343    /// `includeFencedBrokers` below v2.
344    #[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}