Skip to main content

krabka_client_admin/
lib.rs

1//! Admin client for Krabka operators and control-plane services.
2//!
3//! The client targets the active controller and retries selected RPCs on a
4//! refreshed controller connection when the broker returns `NOT_CONTROLLER`.
5//! It supports plaintext by default and the same client-side TLS/SASL security
6//! surface as [`krabka_client_core`] through [`AdminClient::connect_secured`].
7//!
8//! The client is built on `krabka_client_core::Connection`'s typed
9//! `send::<R: ProtocolRequest>`, so request-version negotiation is automatic
10//! through the `ApiVersionTable` that connect time populates. The public
11//! modules cover topic CRUD, listing and description, partition expansion,
12//! config changes and config-resource listing, SCRAM user credentials, ACLs,
13//! client quotas, delegation tokens, metadata-quorum voters, and log-dir
14//! inspection.
15
16use std::{
17    any::Any,
18    collections::BTreeMap,
19    io,
20    net::SocketAddr,
21    sync::{Arc, Mutex, PoisonError},
22};
23
24#[cfg(test)]
25use krabka_client_core::{ClientDnsTimeout, MockBroker};
26use krabka_client_core::{
27    ClientError, Connection, ConnectionOptions, MetadataRecoveryRebootstrapTrigger,
28    MetadataRecoveryStrategy, ProtocolRequest as _, connection_target_host, security,
29    telemetry::{
30        ClientMetrics, ClientTelemetry, ClientTelemetryConfig, NetworkMetrics, TelemetryClientType,
31        TelemetryTiming,
32    },
33    transport,
34};
35use krabka_protocol::owned::{
36    describe_cluster_response::DescribeClusterResponse, metadata_request, metadata_response,
37};
38use krabka_units::{Time, convert::TimeExt as _};
39use thiserror::Error;
40use tokio::{
41    sync::RwLock,
42    time::{Instant, timeout},
43};
44
45pub mod brokers;
46pub mod cluster;
47mod config;
48pub mod config_resources;
49pub mod configs;
50pub mod consumer_group_listings;
51pub mod delegation_tokens;
52pub mod elections;
53pub mod features;
54mod group_coordinators;
55pub mod group_deletions;
56pub mod group_descriptions;
57pub mod groups;
58pub mod log_dirs;
59pub mod offsets;
60mod partition_leaders;
61pub mod quorum;
62pub mod quotas;
63mod retry;
64pub mod share_group_offsets;
65mod telemetry;
66pub mod topic_descriptions;
67pub mod topics;
68pub mod transactions;
69
70#[cfg(test)]
71#[macro_export]
72macro_rules! encode_test_struct_field {
73    ($encoder:ident, $response:ident, $field:ident, $value:expr, $version:expr, $flexible:expr $(,)?) => {
74        $encoder(
75            &$response {
76                $field: $value,
77                ..Default::default()
78            },
79            $version,
80            $flexible,
81        )
82    };
83}
84
85#[cfg(test)]
86#[macro_export]
87macro_rules! encode_test_struct_fields {
88    ($encoder:ident, $response:ident, { $($field:ident: $value:expr),+ $(,)? }, $version:expr, $flexible:expr $(,)?) => {
89        $encoder(
90            &$response {
91                $($field: $value,)+
92                ..Default::default()
93            },
94            $version,
95            $flexible,
96        )
97    };
98}
99pub mod users;
100
101/// Result of applying a `metadata.version` feature update.
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct MetadataVersionUpdate {
104    pub level: i16,
105}
106
107pub use cluster::{ClusterDescription, ClusterNode, DescribeClusterOptions};
108pub use config::{
109    AdminClientConfig, DEFAULT_ADMIN_CONNECTIONS_MAX_IDLE, DEFAULT_ADMIN_REQUEST_TIMEOUT,
110    DEFAULT_API_TIMEOUT, DEFAULT_ENABLE_METRICS_PUSH, DEFAULT_RETRY_BACKOFF,
111    DEFAULT_RETRY_BACKOFF_MAX, DEFAULT_SOCKET_CONNECTION_SETUP_TIMEOUT,
112    DEFAULT_SOCKET_CONNECTION_SETUP_TIMEOUT_MAX,
113};
114pub use config_resources::{ConfigResource, ConfigResourceType};
115pub use configs::{
116    AlterConfigOp, AlterConfigOpType, AlterConfigsResults, Config, ConfigEntry, ConfigSource,
117    ConfigSynonym, ConfigType, DescribeConfigsOptions, DescribeConfigsResults,
118    IncrementalAlterConfigsOptions,
119};
120pub use consumer_group_listings::{
121    ConsumerGroupListing, ListConsumerGroupsOptions, ListConsumerGroupsResult,
122};
123pub use delegation_tokens::{
124    CreateDelegationTokenOptions, DelegationToken, DescribeDelegationTokenOptions,
125    ExpireDelegationTokenOptions, RenewDelegationTokenOptions,
126};
127pub use elections::{ElectionResults, ElectionType};
128pub use features::{
129    DescribeFeaturesOptions, FeatureMetadata, FeatureRange, FeatureUpdate, UpdateFeaturesOptions,
130    UpdateFeaturesResults, UpgradeType,
131};
132pub use group_deletions::GroupDeletions;
133pub use group_descriptions::{
134    ClassicGroupDescription, ConsumerGroupDescription, DescribeGroupsOptions, GroupDescriptions,
135    MemberDescription, ShareGroupDescription, StreamsGroupDescription,
136};
137pub use groups::{
138    ConsumerGroupOffsetOutcome, MemberToRemove, RemoveMembersOptions, RemovedMember, RemovedMembers,
139};
140pub use log_dirs::{
141    BrokerResult, LogDirInfo, LogDirPartitionInfo, LogDirTopicInfo, ReplicaLogDir,
142    ReplicaLogDirInfo, TopicPartitionReplica,
143};
144pub use offsets::{IsolationLevel, ListedOffset, OffsetSpec};
145pub use quorum::{MetadataQuorum, QuorumNode, QuorumReplica, RaftVoterEndpoint};
146pub use quotas::{
147    ClientQuotaAlteration, ClientQuotaEntity, ClientQuotaFilter, ClientQuotaFilterComponent,
148    ClientQuotaMatch, ClientQuotas, ENTITY_CLIENT_ID, ENTITY_IP, ENTITY_USER, QuotaOp,
149    UserQuotaConfig, diff_user_quotas,
150};
151pub use share_group_offsets::{
152    ListShareGroupOffsetsSpec, ShareGroupOffsets, SharePartitionOffsetInfo,
153};
154pub use topic_descriptions::{
155    DescribeTopicsOptions, ListTopicsOptions, TopicDescription, TopicDescriptions, TopicListing,
156    TopicPartitionInfo,
157};
158pub use topics::{
159    AlterPartitionReassignmentsOptions, CreatePartitionsOp, CreatePartitionsOutcome,
160    CreateTopicOutcome, CreateTopicSpec, DeleteRecordsOp, DeleteRecordsOutcome, DeleteTopicOutcome,
161    PartitionAssignment, PartitionAssignmentOutcome, TopicMetadata, TopicMetadataEntry,
162    TopicMutationOptions, TopicReplicationStatus,
163};
164pub use transactions::{
165    AbortTransactionSpec, ListTransactionsFilter, ProducerStateInfo, TransactionDescription,
166    TransactionListing,
167};
168pub use users::{
169    AclEntry, AclEntryFilter, AclOperation, CreateAclOutcome, DEFAULT_SCRAM_ITERATIONS,
170    DeleteAclFilterOutcome, MAX_SCRAM_ITERATIONS, MIN_SCRAM_ITERATIONS, PatternType,
171    PermissionType, ResourceType, ScramDeletion, ScramIterations, ScramUpsertion, ScramUserOutcome,
172    UserScramCredential, UserScramCredentials,
173};
174
175/// Test seam for `AdminClient`.
176///
177/// The operator's reconcile only needs dynamic dispatch through this trait.
178/// Production code wraps a concrete `AdminClient`, and tests substitute a
179/// fake.
180///
181/// Methods take `&mut self` because the underlying `AdminClient`'s
182/// `NOT_CONTROLLER` retry path reconnects the inner `Connection` in place,
183/// which needs unique access.
184#[async_trait::async_trait]
185pub trait AdminClientLike: Send {
186    /// Finalize `metadata.version` at `level` through `UpdateFeatures`.
187    ///
188    /// # Errors
189    /// Returns a transport, protocol, or broker error when Kafka rejects the
190    /// requested level.
191    async fn update_metadata_version(
192        &mut self,
193        _level: i16,
194        _upgrade_type: UpgradeType,
195        _timeout: Time,
196    ) -> Result<MetadataVersionUpdate, AdminError> {
197        Err(AdminError::Protocol(
198            "UpdateFeatures is not implemented by this admin client".into(),
199        ))
200    }
201    /// Return the live `__cluster_metadata` partition-zero quorum view.
202    ///
203    /// # Errors
204    /// Returns a transport, protocol, or Kafka error from `DescribeQuorum`.
205    async fn describe_metadata_quorum(&mut self) -> Result<MetadataQuorum, AdminError> {
206        Err(AdminError::Protocol(
207            "DescribeQuorum is not implemented by this admin client".into(),
208        ))
209    }
210    /// Remove one exact node and directory identity from the metadata quorum,
211    /// as Kafka's `removeRaftVoter` operation does.
212    ///
213    /// `cluster_id` is Kafka's `RemoveRaftVoterOptions.clusterId`, the
214    /// cluster ID string as `meta.properties` holds it; `None` sends a null
215    /// cluster ID, which the controller does not check.
216    ///
217    /// `NOT_CONTROLLER` (41) makes the client find the controller again and
218    /// resend the request with the backoff until `default.api.timeout.ms`
219    /// (60 s), as Kafka's `handleNotControllerError` does.
220    ///
221    /// # Errors
222    /// Returns a transport, protocol, or Kafka error from `RemoveRaftVoter`,
223    /// and `REQUEST_TIMED_OUT` (7) at the deadline.
224    async fn remove_raft_voter(
225        &mut self,
226        _cluster_id: Option<&str>,
227        _node_id: i32,
228        _directory_id: uuid::Uuid,
229    ) -> Result<(), AdminError> {
230        Err(AdminError::Protocol(
231            "RemoveRaftVoter is not implemented by this admin client".into(),
232        ))
233    }
234    /// Metadata for the named topics. Pass an empty slice to fetch all
235    /// topics, per Kafka semantics.
236    ///
237    /// # Errors
238    /// Returns an error when configuration is invalid, protocol encoding fails, the broker rejects the request, or transport I/O fails.
239    async fn metadata(&mut self, topics: &[&str]) -> Result<TopicMetadata, AdminError>;
240    /// Converges every partition of `topic` to `replication_factor` through
241    /// `AlterPartitionReassignments`.
242    ///
243    /// Existing replicas are retained where possible. Added replicas rotate
244    /// across the broker list, and decreases preserve the leading replica
245    /// order. An existing reassignment is never overwritten.
246    ///
247    /// # Errors
248    /// Returns an error when the factor is invalid for the live broker set,
249    /// metadata cannot be read, Kafka rejects a reassignment, or transport I/O
250    /// fails.
251    async fn reconcile_topic_replication_factor(
252        &mut self,
253        topic: &str,
254        replication_factor: i32,
255        timeout: Time,
256    ) -> Result<TopicReplicationStatus, AdminError>;
257    /// Returns current replica membership for every selected partition.
258    ///
259    /// Pass an empty topic slice to inspect the whole cluster before broker
260    /// evacuation.
261    ///
262    /// # Errors
263    /// Returns a transport or protocol error.
264    async fn describe_partition_assignments(
265        &mut self,
266        _topics: &[&str],
267    ) -> Result<Vec<PartitionAssignment>, AdminError> {
268        Err(AdminError::Protocol(
269            "Describe partition assignments is not implemented by this admin client".into(),
270        ))
271    }
272    /// Lists active partition reassignments and their replica membership.
273    ///
274    /// An empty filter lists every active reassignment.
275    ///
276    /// # Errors
277    /// Returns a transport, protocol, or top-level broker error.
278    async fn list_partition_reassignments(
279        &mut self,
280        _partitions: &BTreeMap<String, Vec<i32>>,
281        _timeout: Time,
282    ) -> Result<Vec<PartitionAssignment>, AdminError> {
283        Err(AdminError::Protocol(
284            "ListPartitionReassignments is not implemented by this admin client".into(),
285        ))
286    }
287    /// Submits exact replica lists; `None` cancels an active reassignment.
288    /// A reassignment may change a partition's replication factor, Kafka's
289    /// default; [`AdminClient::alter_partition_assignments_with`] can refuse that.
290    ///
291    /// # Errors
292    /// Returns a transport, protocol, or top-level broker error.
293    async fn alter_partition_assignments(
294        &mut self,
295        _assignments: &BTreeMap<(String, i32), Option<Vec<i32>>>,
296        _timeout: Time,
297    ) -> Result<Vec<PartitionAssignmentOutcome>, AdminError> {
298        Err(AdminError::Protocol(
299            "AlterPartitionReassignments is not implemented by this admin client".into(),
300        ))
301    }
302    /// Unregisters one broker from the active `KRaft` controller.
303    ///
304    /// Kafka's `KafkaAdminClient.unregisterBroker` retries
305    /// `REQUEST_TIMED_OUT` (7) with the retry backoff until the call deadline
306    /// (`default.api.timeout.ms`, 60 s), and fails at once on every other
307    /// code, `NOT_CONTROLLER` (41) too. This call does the same.
308    ///
309    /// # Errors
310    /// Returns a transport, protocol, or broker error. At the deadline it
311    /// gives `REQUEST_TIMED_OUT` (7).
312    async fn unregister_broker(&mut self, _broker_id: i32) -> Result<(), AdminError> {
313        Err(AdminError::Protocol(
314            "UnregisterBroker is not implemented by this admin client".into(),
315        ))
316    }
317    /// Creates topics through the active controller, as Kafka's
318    /// `KafkaAdminClient.createTopics` does.
319    ///
320    /// The call retries until the deadline of `options`:
321    ///
322    /// - `NOT_CONTROLLER` (41) for any topic: find the controller again, wait
323    ///   for the backoff, and send every topic that has no outcome again
324    ///   (`handleNotControllerError`). With controller bootstrap,
325    ///   `NOT_LEADER_OR_FOLLOWER` (6) does the same.
326    /// - `THROTTLING_QUOTA_EXCEEDED` (89): when
327    ///   [`TopicMutationOptions::retry_on_quota_violation`] is set, wait for the
328    ///   throttle time of the response and send only the throttled topics
329    ///   again. A topic that has an outcome is not sent again.
330    ///
331    /// At the deadline a throttled topic keeps code 89, with the throttle time
332    /// that remains. Another topic without an outcome gets
333    /// `REQUEST_TIMED_OUT` (7). The outcomes keep the order of `specs`.
334    ///
335    /// # Errors
336    /// Returns an error when configuration is invalid, protocol encoding fails, or transport I/O fails.
337    async fn create_topics(
338        &mut self,
339        specs: &[CreateTopicSpec],
340        options: TopicMutationOptions,
341    ) -> Result<Vec<CreateTopicOutcome>, AdminError>;
342    /// Deletes topics through the active controller, as Kafka's
343    /// `KafkaAdminClient.deleteTopics` does. The call retries
344    /// `NOT_CONTROLLER` (41) and `THROTTLING_QUOTA_EXCEEDED` (89) as
345    /// [`AdminClientLike::create_topics`] does.
346    ///
347    /// # Errors
348    /// Returns an error when configuration is invalid, protocol encoding fails, or transport I/O fails.
349    async fn delete_topics(
350        &mut self,
351        names: &[&str],
352        options: TopicMutationOptions,
353    ) -> Result<Vec<DeleteTopicOutcome>, AdminError>;
354    /// Adds partitions through the active controller, as Kafka's
355    /// `KafkaAdminClient.createPartitions` does. The call retries
356    /// `NOT_CONTROLLER` (41) and `THROTTLING_QUOTA_EXCEEDED` (89) as
357    /// [`AdminClientLike::create_topics`] does.
358    ///
359    /// # Errors
360    /// Returns an error when configuration is invalid, protocol encoding fails, or transport I/O fails.
361    async fn create_partitions(
362        &mut self,
363        ops: &[CreatePartitionsOp],
364        options: TopicMutationOptions,
365    ) -> Result<Vec<CreatePartitionsOutcome>, AdminError>;
366    /// Deletes records below each requested partition offset.
367    ///
368    /// `offset == -1` follows Kafka `DeleteRecords` semantics and truncates to
369    /// the partition high watermark. The returned `low_watermark` is the
370    /// broker's resulting log-start offset for that partition.
371    ///
372    /// # Errors
373    ///
374    /// Returns an [`AdminError`] when metadata lookup, leader routing, transport,
375    /// or protocol handling fails. Kafka partition-level failures remain in the
376    /// returned [`DeleteRecordsOutcome::error_code`].
377    async fn delete_records(
378        &mut self,
379        ops: &[DeleteRecordsOp],
380        timeout: Time,
381    ) -> Result<Vec<DeleteRecordsOutcome>, AdminError>;
382    /// Describes the configs of each resource of `resources`, as Kafka's
383    /// `describeConfigs` operation does.
384    ///
385    /// The client sends one `DescribeConfigs` request to each broker that a
386    /// `BROKER` resource with a broker id or a `BROKER_LOGGER` resource
387    /// names, and one request with every other resource on its connection,
388    /// all at the same time. Each request asks for every config of its
389    /// resources. The result has one entry for each resource:
390    ///
391    /// - Its [`Config`], or the error the broker gave for it, such as
392    ///   `UNKNOWN_TOPIC_OR_PARTITION` (3).
393    /// - `UNKNOWN_SERVER_ERROR` (-1) when the answer has no result for it.
394    /// - The error of its request when that request fails: `REQUEST_TIMED_OUT`
395    ///   (7) at `default.api.timeout.ms` (60 s), or `UNSUPPORTED_VERSION` (35)
396    ///   when the node does not support `DescribeConfigs` v3 and
397    ///   `include_documentation` is set. A broker is found by id in fresh
398    ///   metadata, and a missing broker or a lost connection is tried again
399    ///   until the deadline, as in [`AdminClient::describe_log_dirs`].
400    ///
401    /// # Errors
402    /// Returns [`AdminError::InvalidArgument`], and sends nothing, when the
403    /// name of a `BROKER` resource is neither empty nor a broker id, or the
404    /// name of a `BROKER_LOGGER` resource is not a broker id, as Kafka's
405    /// `nodeFor` throws a `NumberFormatException`.
406    async fn describe_configs(
407        &mut self,
408        resources: &[ConfigResource],
409        options: DescribeConfigsOptions,
410    ) -> Result<DescribeConfigsResults, AdminError>;
411    /// Changes the configs of each resource of `configs`, as Kafka's
412    /// `incrementalAlterConfigs` operation does. With
413    /// [`IncrementalAlterConfigsOptions::validate_only`] the broker
414    /// validates the changes and applies none.
415    ///
416    /// A `BROKER` resource with a broker id goes to that broker, and a
417    /// `BROKER_LOGGER` resource to its broker, each in a request of its own.
418    /// With a controller bootstrap only a `BROKER_LOGGER` resource goes to
419    /// its node. Every other resource goes in one request on the client's
420    /// connection. A `NOT_CONTROLLER` (41) answer, or with a controller
421    /// bootstrap a `NOT_LEADER_OR_FOLLOWER` (6) answer, makes the client find
422    /// the controller again and send the request again until the deadline.
423    /// The result has one entry for each resource:
424    ///
425    /// - `Ok(())`, or the error the broker gave for it, such as
426    ///   `INVALID_CONFIG` (40).
427    /// - `UNKNOWN_SERVER_ERROR` (-1) when the answer has no result for it.
428    /// - The error of its request when that request fails, with
429    ///   `REQUEST_TIMED_OUT` (7) at `default.api.timeout.ms` (60 s).
430    ///
431    /// # Errors
432    /// Returns [`AdminError::InvalidArgument`], and sends nothing, for a
433    /// resource name that [`Self::describe_configs`] refuses.
434    async fn incremental_alter_configs(
435        &mut self,
436        configs: &BTreeMap<ConfigResource, Vec<AlterConfigOp>>,
437        options: IncrementalAlterConfigsOptions,
438    ) -> Result<AlterConfigsResults, AdminError>;
439    /// Upserts SCRAM-SHA-512 credentials, deletes them, or both, in a single
440    /// call.
441    ///
442    /// `upsertions` carry plaintext passwords. The function generates a fresh
443    /// 16-byte salt per row and computes the KIP-554 wire `salted_password`
444    /// (PBKDF2-HMAC-SHA-512) client-side with
445    /// `krabka_security::pbkdf2_salted`. The broker never sees the raw
446    /// password.
447    ///
448    /// # Errors
449    /// Returns an error when configuration is invalid, protocol encoding fails, the broker rejects the request, or transport I/O fails.
450    async fn alter_user_scram_credentials_sha512(
451        &mut self,
452        upsertions: &[ScramUpsertion],
453        deletions: &[ScramDeletion],
454    ) -> Result<Vec<ScramUserOutcome>, AdminError>;
455    /// SCRAM-SHA-256 sibling of
456    /// [`Self::alter_user_scram_credentials_sha512`].
457    ///
458    /// The iteration counts, the salt generation, and the salted-password
459    /// derivation are identical to the SHA-512 path. Only the mechanism wire
460    /// byte and the HMAC algorithm differ.
461    ///
462    /// # Errors
463    ///
464    /// Same as `_sha512`: returns [`AdminError::Protocol`] when the
465    /// system RNG fails, otherwise propagates the broker's
466    /// per-username outcome rows.
467    async fn alter_user_scram_credentials_sha256(
468        &mut self,
469        upsertions: &[ScramUpsertion],
470        deletions: &[ScramDeletion],
471    ) -> Result<Vec<ScramUserOutcome>, AdminError>;
472    /// Lists ACLs that match `filter`.
473    ///
474    /// The broker's response is resource-grouped on the wire, with one block
475    /// per `(resource_type, resource_name, pattern_type)`. This method
476    /// flattens it back into `AclEntry` rows for the diff.
477    ///
478    /// # Errors
479    /// Returns an error when configuration is invalid, protocol encoding fails, the broker rejects the request, or transport I/O fails.
480    async fn describe_acls(&mut self, filter: &AclEntryFilter)
481    -> Result<Vec<AclEntry>, AdminError>;
482    /// Creates the supplied ACLs.
483    ///
484    /// # Errors
485    /// Returns [`AdminError::InvalidArgument`], before anything is sent, for
486    /// an entry whose pattern type is [`PatternType::Match`]: Kafka's
487    /// `ResourcePattern` refuses `patternType must not be MATCH`. Otherwise
488    /// returns an error when protocol encoding fails, the broker rejects the
489    /// request, or transport I/O fails.
490    async fn create_acls(
491        &mut self,
492        creations: &[AclEntry],
493    ) -> Result<Vec<CreateAclOutcome>, AdminError>;
494    /// Deletes every ACL that matches any of `filters`.
495    ///
496    /// Each filter's response surfaces the matched ACL set, so callers can
497    /// confirm that the deletion converged on the expected rows.
498    ///
499    /// # Errors
500    /// Returns an error when configuration is invalid, protocol encoding fails, the broker rejects the request, or transport I/O fails.
501    async fn delete_acls(
502        &mut self,
503        filters: &[AclEntryFilter],
504    ) -> Result<Vec<DeleteAclFilterOutcome>, AdminError>;
505    /// Reads the quotas of the user entity `{user: username}`.
506    ///
507    /// The filter is strict on the single component `("user", username)`, as
508    /// `ClientQuotaFilter.containsOnly`: an entry whose entity also has a
509    /// `client-id` does not match.
510    ///
511    /// # Errors
512    /// See [`AdminClient::describe_client_quotas`].
513    async fn describe_user_quotas(&mut self, username: &str)
514    -> Result<UserQuotaConfig, AdminError>;
515    /// Applies `ops` to the user entity `{user: username}`. Returns the
516    /// error the broker gave for the entity, or `None` on success. No
517    /// request is sent for an empty `ops`.
518    ///
519    /// # Errors
520    /// See [`AdminClient::alter_client_quotas`].
521    async fn alter_user_quotas(
522        &mut self,
523        username: &str,
524        ops: &[QuotaOp],
525        validate_only: bool,
526    ) -> Result<Option<KafkaError>, AdminError>;
527
528    // ── delegation-token RPCs (KIP-48) ────────────────────────────────
529    //
530    // Each method runs the `_impl` call of the same name in
531    // `delegation_tokens.rs`.
532
533    /// Creates a delegation token, as Kafka's `createDelegationToken`
534    /// operation does.
535    ///
536    /// The broker does not return the renewers, so the result has the
537    /// renewers of `options`, as Kafka's result does. With an
538    /// [`owner`](CreateDelegationTokenOptions::owner) the client negotiates
539    /// `CreateDelegationToken` v3 or higher, as Kafka's request refuses to
540    /// write the owner fields below v3.
541    ///
542    /// # Errors
543    ///
544    /// Returns [`AdminError::Broker`] for a non-zero error code, such as
545    /// `DELEGATION_TOKEN_AUTH_DISABLED` (61),
546    /// `DELEGATION_TOKEN_REQUEST_NOT_ALLOWED` (64),
547    /// `DELEGATION_TOKEN_AUTHORIZATION_FAILED` (65), or
548    /// `INVALID_PRINCIPAL_TYPE` (67). Returns [`AdminError::Transport`] when
549    /// the connection fails, or when an owner is set and the broker does not
550    /// support v3.
551    async fn create_delegation_token(
552        &mut self,
553        options: &CreateDelegationTokenOptions,
554    ) -> Result<DelegationToken, AdminError>;
555    /// Renews the delegation token with `hmac`, as Kafka's
556    /// `renewDelegationToken` operation does, and returns its new expiry in
557    /// epoch milliseconds.
558    ///
559    /// # Errors
560    ///
561    /// Returns [`AdminError::Broker`] for a non-zero error code, such as
562    /// `DELEGATION_TOKEN_NOT_FOUND` (62), `DELEGATION_TOKEN_OWNER_MISMATCH`
563    /// (63), or `DELEGATION_TOKEN_EXPIRED` (66). Returns
564    /// [`AdminError::Transport`] when the connection fails.
565    async fn renew_delegation_token(
566        &mut self,
567        hmac: &[u8],
568        options: RenewDelegationTokenOptions,
569    ) -> Result<i64, AdminError>;
570    /// Expires the delegation token with `hmac`, as Kafka's
571    /// `expireDelegationToken` operation does, and returns the expiry that
572    /// the broker set, in epoch milliseconds. A token that expires now gets
573    /// the broker's clock at the time of the request.
574    ///
575    /// # Errors
576    ///
577    /// Returns [`AdminError::Broker`] for a non-zero error code, such as
578    /// `DELEGATION_TOKEN_NOT_FOUND` (62), `DELEGATION_TOKEN_OWNER_MISMATCH`
579    /// (63), or `DELEGATION_TOKEN_EXPIRED` (66). Returns
580    /// [`AdminError::Transport`] when the connection fails.
581    async fn expire_delegation_token(
582        &mut self,
583        hmac: &[u8],
584        options: ExpireDelegationTokenOptions,
585    ) -> Result<i64, AdminError>;
586    /// Describes the delegation tokens that `options` selects, as Kafka's
587    /// `describeDelegationToken` operation does.
588    ///
589    /// # Errors
590    ///
591    /// Returns [`AdminError::Broker`] for a non-zero error code, such as
592    /// `DELEGATION_TOKEN_AUTH_DISABLED` (61) or
593    /// `DELEGATION_TOKEN_REQUEST_NOT_ALLOWED` (64). Returns
594    /// [`AdminError::Transport`] when the connection fails.
595    async fn describe_delegation_token(
596        &mut self,
597        options: &DescribeDelegationTokenOptions,
598    ) -> Result<Vec<DelegationToken>, AdminError>;
599}
600
601#[async_trait::async_trait]
602impl AdminClientLike for AdminClient {
603    async fn update_metadata_version(
604        &mut self,
605        level: i16,
606        upgrade_type: UpgradeType,
607        timeout: Time,
608    ) -> Result<MetadataVersionUpdate, AdminError> {
609        Self::update_metadata_version_impl(self, level, upgrade_type, timeout).await
610    }
611
612    async fn describe_metadata_quorum(&mut self) -> Result<MetadataQuorum, AdminError> {
613        Self::describe_metadata_quorum_impl(self).await
614    }
615
616    async fn remove_raft_voter(
617        &mut self,
618        cluster_id: Option<&str>,
619        node_id: i32,
620        directory_id: uuid::Uuid,
621    ) -> Result<(), AdminError> {
622        Self::remove_raft_voter_impl(self, cluster_id, node_id, directory_id).await
623    }
624
625    async fn metadata(&mut self, topics: &[&str]) -> Result<TopicMetadata, AdminError> {
626        Self::metadata_impl(self, topics).await
627    }
628    async fn reconcile_topic_replication_factor(
629        &mut self,
630        topic: &str,
631        replication_factor: i32,
632        timeout: Time,
633    ) -> Result<TopicReplicationStatus, AdminError> {
634        Self::reconcile_topic_replication_factor_impl(self, topic, replication_factor, timeout)
635            .await
636    }
637    async fn describe_partition_assignments(
638        &mut self,
639        topics: &[&str],
640    ) -> Result<Vec<PartitionAssignment>, AdminError> {
641        Self::describe_partition_assignments_impl(self, topics).await
642    }
643    async fn list_partition_reassignments(
644        &mut self,
645        partitions: &BTreeMap<String, Vec<i32>>,
646        timeout: Time,
647    ) -> Result<Vec<PartitionAssignment>, AdminError> {
648        Self::list_partition_reassignments_impl(self, partitions, timeout).await
649    }
650    async fn alter_partition_assignments(
651        &mut self,
652        assignments: &BTreeMap<(String, i32), Option<Vec<i32>>>,
653        timeout: Time,
654    ) -> Result<Vec<PartitionAssignmentOutcome>, AdminError> {
655        Self::alter_partition_assignments_impl(self, assignments, timeout).await
656    }
657    async fn unregister_broker(&mut self, broker_id: i32) -> Result<(), AdminError> {
658        Self::unregister_broker_impl(self, broker_id).await
659    }
660    async fn create_topics(
661        &mut self,
662        specs: &[CreateTopicSpec],
663        options: TopicMutationOptions,
664    ) -> Result<Vec<CreateTopicOutcome>, AdminError> {
665        Self::create_topics_impl(self, specs, options).await
666    }
667    async fn delete_topics(
668        &mut self,
669        names: &[&str],
670        options: TopicMutationOptions,
671    ) -> Result<Vec<DeleteTopicOutcome>, AdminError> {
672        Self::delete_topics_impl(self, names, options).await
673    }
674    async fn create_partitions(
675        &mut self,
676        ops: &[CreatePartitionsOp],
677        options: TopicMutationOptions,
678    ) -> Result<Vec<CreatePartitionsOutcome>, AdminError> {
679        Self::create_partitions_impl(self, ops, options).await
680    }
681    async fn delete_records(
682        &mut self,
683        ops: &[DeleteRecordsOp],
684        timeout: Time,
685    ) -> Result<Vec<DeleteRecordsOutcome>, AdminError> {
686        Self::delete_records_impl(self, ops, timeout).await
687    }
688    async fn describe_configs(
689        &mut self,
690        resources: &[ConfigResource],
691        options: DescribeConfigsOptions,
692    ) -> Result<DescribeConfigsResults, AdminError> {
693        Self::describe_configs_impl(self, resources, options).await
694    }
695    async fn incremental_alter_configs(
696        &mut self,
697        configs: &BTreeMap<ConfigResource, Vec<AlterConfigOp>>,
698        options: IncrementalAlterConfigsOptions,
699    ) -> Result<AlterConfigsResults, AdminError> {
700        Self::incremental_alter_configs_impl(self, configs, options).await
701    }
702    async fn alter_user_scram_credentials_sha512(
703        &mut self,
704        upsertions: &[ScramUpsertion],
705        deletions: &[ScramDeletion],
706    ) -> Result<Vec<ScramUserOutcome>, AdminError> {
707        Self::alter_user_scram_credentials_sha512_impl(self, upsertions, deletions).await
708    }
709    async fn alter_user_scram_credentials_sha256(
710        &mut self,
711        upsertions: &[ScramUpsertion],
712        deletions: &[ScramDeletion],
713    ) -> Result<Vec<ScramUserOutcome>, AdminError> {
714        Self::alter_user_scram_credentials_sha256_impl(self, upsertions, deletions).await
715    }
716    async fn describe_acls(
717        &mut self,
718        filter: &AclEntryFilter,
719    ) -> Result<Vec<AclEntry>, AdminError> {
720        Self::describe_acls_impl(self, filter).await
721    }
722    async fn create_acls(
723        &mut self,
724        creations: &[AclEntry],
725    ) -> Result<Vec<CreateAclOutcome>, AdminError> {
726        Self::create_acls_impl(self, creations).await
727    }
728    async fn delete_acls(
729        &mut self,
730        filters: &[AclEntryFilter],
731    ) -> Result<Vec<DeleteAclFilterOutcome>, AdminError> {
732        Self::delete_acls_impl(self, filters).await
733    }
734    async fn describe_user_quotas(
735        &mut self,
736        username: &str,
737    ) -> Result<UserQuotaConfig, AdminError> {
738        Self::describe_user_quotas_impl(self, username).await
739    }
740    async fn alter_user_quotas(
741        &mut self,
742        username: &str,
743        ops: &[QuotaOp],
744        validate_only: bool,
745    ) -> Result<Option<KafkaError>, AdminError> {
746        Self::alter_user_quotas_impl(self, username, ops, validate_only).await
747    }
748
749    async fn create_delegation_token(
750        &mut self,
751        options: &CreateDelegationTokenOptions,
752    ) -> Result<DelegationToken, AdminError> {
753        Self::create_delegation_token_impl(self, options).await
754    }
755    async fn renew_delegation_token(
756        &mut self,
757        hmac: &[u8],
758        options: RenewDelegationTokenOptions,
759    ) -> Result<i64, AdminError> {
760        Self::renew_delegation_token_impl(self, hmac, options).await
761    }
762    async fn expire_delegation_token(
763        &mut self,
764        hmac: &[u8],
765        options: ExpireDelegationTokenOptions,
766    ) -> Result<i64, AdminError> {
767        Self::expire_delegation_token_impl(self, hmac, options).await
768    }
769    async fn describe_delegation_token(
770        &mut self,
771        options: &DescribeDelegationTokenOptions,
772    ) -> Result<Vec<DelegationToken>, AdminError> {
773        Self::describe_delegation_token_impl(self, options).await
774    }
775}
776
777#[derive(Debug, Error)]
778pub enum AdminError {
779    /// No bootstrap address gave a usable connection. `source` holds the
780    /// error from the last address tried, and is `None` only when the list
781    /// was empty. Match it to tell an unreachable or silent broker
782    /// ([`ClientError::Connect`] or [`ClientError::Timeout`]) from a TLS or
783    /// SASL failure with no verdict from the peer ([`ClientError::Tls`] or
784    /// [`ClientError::Sasl`]).
785    ///
786    /// A rejected authentication does not give this error. The client stops
787    /// at the first address that rejects it and returns
788    /// `AdminError::Transport(ClientError::Authentication { .. })`, as Kafka's
789    /// `AdminMetadataManager` makes an `AuthenticationException` fatal.
790    #[error("no bootstrap address connected: tried {tried}{cause}",
791            cause = .source.as_ref().map(|e| format!("; last error: {e}")).unwrap_or_default())]
792    Connect {
793        tried: usize,
794        #[source]
795        source: Option<Box<Self>>,
796    },
797    /// A setting of [`AdminClientConfig`] is invalid, as Kafka raises a
798    /// `ConfigException` when it builds the admin client.
799    #[error("invalid admin client configuration: {0}")]
800    InvalidConfig(String),
801    /// An argument of a call is invalid, as Kafka's admin types throw an
802    /// `IllegalArgumentException` before anything is sent.
803    #[error("invalid argument: {0}")]
804    InvalidArgument(String),
805    #[error("broker returned error: api={api} code={code} ({name}){detail}",
806            detail = .message.as_deref().map(|m| format!(" {m:?}")).unwrap_or_default())]
807    Broker {
808        api: &'static str,
809        code: i16,
810        name: &'static str,
811        message: Option<String>,
812    },
813    #[error("client-core: {0}")]
814    Transport(#[from] ClientError),
815    #[error("protocol: {0}")]
816    Protocol(String),
817}
818
819impl AdminError {
820    /// Whether a peer rejected authentication. Kafka's admin client fails the
821    /// call with the `AuthenticationException` and does not retry
822    /// (`KafkaAdminClient` `handleResponses`, `AdminMetadataManager.updateFailed`).
823    #[must_use]
824    pub const fn is_authentication_failure(&self) -> bool {
825        matches!(self, Self::Transport(error) if error.is_authentication_failure())
826    }
827}
828
829/// A Kafka-level error attached to a single per-resource outcome.
830#[derive(Debug, Clone, PartialEq, Eq)]
831pub struct KafkaError {
832    pub code: i16,
833    pub name: &'static str,
834    pub message: Option<String>,
835}
836
837pub(crate) fn kafka_error_if(code: i16, message: Option<String>) -> Option<KafkaError> {
838    if code == 0 {
839        None
840    } else {
841        Some(KafkaError {
842            code,
843            name: kafka_error_name(code),
844            message,
845        })
846    }
847}
848
849async fn lookup_first<F, I>(
850    host_port: &str,
851    dns_timeout: krabka_client_core::ClientDnsTimeout,
852    lookup: F,
853) -> Result<SocketAddr, AdminError>
854where
855    F: Future<Output = io::Result<I>>,
856    I: IntoIterator<Item = SocketAddr>,
857{
858    let addrs = timeout(dns_timeout.time().to_std(), lookup)
859        .await
860        .map_err(|_| {
861            AdminError::Protocol(format!(
862                "DNS lookup {host_port} timed out after {} ms",
863                dns_timeout.milliseconds(),
864            ))
865        })?
866        .map_err(|error| AdminError::Protocol(format!("DNS lookup {host_port}: {error}")))?;
867    addrs
868        .into_iter()
869        .next()
870        .ok_or_else(|| AdminError::Protocol(format!("no addresses for {host_port}")))
871}
872
873fn format_host_port(host: &str, port: i32) -> String {
874    if host.contains(':') && !host.starts_with('[') {
875        format!("[{host}]:{port}")
876    } else {
877        format!("{host}:{port}")
878    }
879}
880
881/// Short-lived admin client that targets one cluster's controller. It can
882/// negotiate TLS/SASL through [`AdminClient::connect_secured`].
883///
884/// As Kafka's admin client with `enable.metrics.push` on (the default), it
885/// pushes the client metrics that a broker's client metrics subscription
886/// names (KIP-714) on its bootstrap connection, and its connections count in
887/// the `admin-client-metrics` network metrics.
888/// [`AdminClientConfig::enable_metrics_push`] turns this off. The
889/// constructors without an [`AdminClientConfig`] keep Kafka's default.
890/// [`close`](Self::close) sends the terminating push and waits for it;
891/// dropping the client sends it in the background.
892pub struct AdminClient {
893    pub(crate) conn: Arc<RecoveringConnection>,
894    bootstrap_addrs: Vec<String>,
895    /// Full connection template carried forward so reconnects preserve
896    /// caller-supplied identity, security, and timeouts. With metrics push
897    /// on, it carries the network metrics that each new connection counts in.
898    options: ConnectionOptions,
899    /// The deadline, backoff and retry count of each call
900    /// (`default.api.timeout.ms`, `retry.backoff.ms`, `retry.backoff.max.ms`,
901    /// `retries`).
902    pub(crate) retry: retry::RetryPolicy,
903    /// The KIP-714 reporter, `None` with `enable_metrics_push` off. Dropping
904    /// it starts the terminating push.
905    telemetry: Option<ClientTelemetry>,
906}
907
908#[derive(Clone, Copy, Debug, Eq, PartialEq)]
909enum BootstrapTarget {
910    Brokers,
911    Controllers,
912}
913
914struct ConnectedTarget {
915    connection: Connection,
916    discovered_addrs: Vec<String>,
917}
918
919/// One controller connection with KIP-919/KIP-1102 bootstrap recovery.
920pub(crate) struct RecoveringConnection {
921    inner: RwLock<Arc<Connection>>,
922    bootstrap_addrs: Vec<String>,
923    options: ConnectionOptions,
924    strategy: MetadataRecoveryStrategy,
925    trigger: MetadataRecoveryRebootstrapTrigger,
926    target: BootstrapTarget,
927    known_addrs: Mutex<Vec<String>>,
928    first_metadata_attempt: Mutex<Option<Instant>>,
929}
930
931impl RecoveringConnection {
932    fn new(
933        connection: Connection,
934        bootstrap_addrs: Vec<String>,
935        options: ConnectionOptions,
936        strategy: MetadataRecoveryStrategy,
937        trigger: MetadataRecoveryRebootstrapTrigger,
938        target: BootstrapTarget,
939        known_addrs: Vec<String>,
940    ) -> Self {
941        Self {
942            inner: RwLock::new(Arc::new(connection)),
943            bootstrap_addrs,
944            options,
945            strategy,
946            trigger,
947            target,
948            known_addrs: Mutex::new(known_addrs),
949            first_metadata_attempt: Mutex::new(None),
950        }
951    }
952
953    pub(crate) async fn send<R>(&self, request: R) -> Result<R::Response, AdminError>
954    where
955        R: krabka_protocol::ProtocolRequest + Clone,
956        R::Response: 'static,
957    {
958        self.send_with_min_version(request, None).await
959    }
960
961    pub(crate) async fn send_at_least<R>(
962        &self,
963        request: R,
964        min_version: i16,
965    ) -> Result<R::Response, AdminError>
966    where
967        R: krabka_protocol::ProtocolRequest + Clone,
968        R::Response: 'static,
969    {
970        self.send_with_min_version(request, Some(min_version)).await
971    }
972
973    async fn send_with_min_version<R>(
974        &self,
975        request: R,
976        min_version: Option<i16>,
977    ) -> Result<R::Response, AdminError>
978    where
979        R: krabka_protocol::ProtocolRequest + Clone,
980        R::Response: 'static,
981    {
982        self.require_advertised_controller_api::<R>().await?;
983        self.begin_metadata_attempt::<R>();
984        let first = self.send_current(request.clone(), min_version).await;
985        match first {
986            Ok(response) if Self::response_requires_rebootstrap(&response) => {
987                if self.strategy == MetadataRecoveryStrategy::None {
988                    return Err(AdminError::Transport(ClientError::Server {
989                        error_code: 129,
990                    }));
991                }
992                self.send_after_rebootstrap(request, min_version).await
993            }
994            Ok(response) if self.metadata_attempt_timed_out(&response) => {
995                self.send_after_rebootstrap(request, min_version).await
996            }
997            Ok(response) => {
998                self.observe_cluster_endpoints(&response);
999                self.complete_metadata_attempt(&response);
1000                Ok(response)
1001            }
1002            Err(error) if AdminClient::is_retriable_transport_error(&error) => {
1003                match self
1004                    .send_after_current_metadata(request.clone(), min_version)
1005                    .await
1006                {
1007                    Ok(response) => Ok(response),
1008                    Err(retry_error) if retry_error.is_authentication_failure() => Err(retry_error),
1009                    Err(_) if self.strategy == MetadataRecoveryStrategy::Rebootstrap => {
1010                        self.send_after_rebootstrap(request, min_version).await
1011                    }
1012                    Err(_) => Err(AdminError::from(error)),
1013                }
1014            }
1015            Err(error) => Err(AdminError::from(error)),
1016        }
1017    }
1018
1019    async fn replace(&self, connection: Connection) {
1020        *self.inner.write().await = Arc::new(connection);
1021    }
1022
1023    async fn send_current<R>(
1024        &self,
1025        request: R,
1026        min_version: Option<i16>,
1027    ) -> Result<R::Response, ClientError>
1028    where
1029        R: krabka_protocol::ProtocolRequest,
1030    {
1031        let connection = self.inner.read().await;
1032        if let Some(min_version) = min_version {
1033            return send_connection_at_least(&connection, request, min_version).await;
1034        }
1035        connection.send(request).await
1036    }
1037
1038    async fn send_after_rebootstrap<R>(
1039        &self,
1040        request: R,
1041        min_version: Option<i16>,
1042    ) -> Result<R::Response, AdminError>
1043    where
1044        R: krabka_protocol::ProtocolRequest,
1045        R::Response: 'static,
1046    {
1047        self.rebootstrap().await?;
1048        self.require_advertised_controller_api::<R>().await?;
1049        self.begin_metadata_attempt::<R>();
1050        let response = self.send_current(request, min_version).await?;
1051        if Self::response_requires_rebootstrap(&response) {
1052            return Err(AdminError::Transport(ClientError::Server {
1053                error_code: 129,
1054            }));
1055        }
1056        self.observe_cluster_endpoints(&response);
1057        self.complete_metadata_attempt(&response);
1058        Ok(response)
1059    }
1060
1061    async fn send_after_current_metadata<R>(
1062        &self,
1063        request: R,
1064        min_version: Option<i16>,
1065    ) -> Result<R::Response, AdminError>
1066    where
1067        R: krabka_protocol::ProtocolRequest + Clone,
1068        R::Response: 'static,
1069    {
1070        self.reconnect_current_metadata().await?;
1071        self.require_advertised_controller_api::<R>().await?;
1072        let response = self.send_current(request.clone(), min_version).await?;
1073        if Self::response_requires_rebootstrap(&response) {
1074            if self.strategy == MetadataRecoveryStrategy::Rebootstrap {
1075                return self.send_after_rebootstrap(request, min_version).await;
1076            }
1077            return Err(AdminError::Transport(ClientError::Server {
1078                error_code: 129,
1079            }));
1080        }
1081        if self.metadata_attempt_timed_out(&response) {
1082            return self.send_after_rebootstrap(request, min_version).await;
1083        }
1084        self.observe_cluster_endpoints(&response);
1085        self.complete_metadata_attempt(&response);
1086        Ok(response)
1087    }
1088
1089    async fn require_advertised_controller_api<R>(&self) -> Result<(), AdminError>
1090    where
1091        R: krabka_protocol::ProtocolRequest,
1092    {
1093        const UNSUPPORTED_ENDPOINT_TYPE: i16 = 115;
1094        if self.target == BootstrapTarget::Controllers
1095            && self
1096                .inner
1097                .read()
1098                .await
1099                .advertised_api_range(R::API_KEY)
1100                .is_none()
1101        {
1102            return Err(AdminError::Broker {
1103                api: "ControllerEndpoint",
1104                code: UNSUPPORTED_ENDPOINT_TYPE,
1105                name: kafka_error_name(UNSUPPORTED_ENDPOINT_TYPE),
1106                message: Some(format!(
1107                    "api_key {} is not supported by the controller listener",
1108                    R::API_KEY
1109                )),
1110            });
1111        }
1112        Ok(())
1113    }
1114
1115    pub(crate) async fn rebootstrap(&self) -> Result<(), AdminError> {
1116        let mut last_error = None;
1117        for host_port in &self.bootstrap_addrs {
1118            match AdminClient::connect_target_one_with_discovery(
1119                host_port,
1120                self.options.clone(),
1121                self.target,
1122            )
1123            .await
1124            {
1125                Ok(connected) => {
1126                    self.replace(connected.connection).await;
1127                    self.replace_known_addrs(if connected.discovered_addrs.is_empty() {
1128                        self.bootstrap_addrs.clone()
1129                    } else {
1130                        connected.discovered_addrs
1131                    });
1132                    self.clear_metadata_attempt();
1133                    return Ok(());
1134                }
1135                Err(error) if error.is_authentication_failure() => return Err(error),
1136                Err(error) => last_error = Some(Box::new(error)),
1137            }
1138        }
1139        Err(AdminError::Connect {
1140            tried: self.bootstrap_addrs.len(),
1141            source: last_error,
1142        })
1143    }
1144
1145    async fn reconnect_current_metadata(&self) -> Result<(), AdminError> {
1146        let known_addrs = self
1147            .known_addrs
1148            .lock()
1149            .unwrap_or_else(PoisonError::into_inner)
1150            .clone();
1151        let mut last_error = None;
1152        for host_port in &known_addrs {
1153            match AdminClient::connect_target_one_with_discovery(
1154                host_port,
1155                self.options.clone(),
1156                self.target,
1157            )
1158            .await
1159            {
1160                Ok(connected) => {
1161                    self.replace(connected.connection).await;
1162                    if !connected.discovered_addrs.is_empty() {
1163                        self.replace_known_addrs(connected.discovered_addrs);
1164                    }
1165                    return Ok(());
1166                }
1167                Err(error) if error.is_authentication_failure() => return Err(error),
1168                Err(error) => last_error = Some(Box::new(error)),
1169            }
1170        }
1171        Err(AdminError::Connect {
1172            tried: known_addrs.len(),
1173            source: last_error,
1174        })
1175    }
1176
1177    fn replace_known_addrs(&self, addrs: Vec<String>) {
1178        *self
1179            .known_addrs
1180            .lock()
1181            .unwrap_or_else(PoisonError::into_inner) = addrs;
1182    }
1183
1184    fn observe_cluster_endpoints<T: Any>(&self, response: &T) {
1185        if let Some(metadata) =
1186            (response as &dyn Any).downcast_ref::<metadata_response::MetadataResponse>()
1187        {
1188            let endpoints = metadata
1189                .brokers
1190                .iter()
1191                .filter(|broker| !broker.host.is_empty() && broker.port > 0)
1192                .map(|broker| format_host_port(&broker.host, broker.port))
1193                .collect::<Vec<_>>();
1194            if !endpoints.is_empty() {
1195                self.replace_known_addrs(endpoints);
1196            }
1197            return;
1198        }
1199        if self.target == BootstrapTarget::Controllers
1200            && let Some(cluster) = (response as &dyn Any).downcast_ref::<DescribeClusterResponse>()
1201        {
1202            let endpoints = cluster
1203                .brokers
1204                .iter()
1205                .filter(|controller| !controller.host.is_empty() && controller.port > 0)
1206                .map(|controller| format_host_port(&controller.host, controller.port))
1207                .collect::<Vec<_>>();
1208            if !endpoints.is_empty() {
1209                self.replace_known_addrs(endpoints);
1210            }
1211        }
1212    }
1213
1214    pub(crate) fn uses_controller_bootstrap(&self) -> bool {
1215        self.target == BootstrapTarget::Controllers
1216    }
1217
1218    /// The `(min, max)` versions of `api_key` that the current peer
1219    /// advertised, or `None` when it did not advertise the API.
1220    pub(crate) async fn advertised_api_range(&self, api_key: i16) -> Option<(i16, i16)> {
1221        self.inner.read().await.advertised_api_range(api_key)
1222    }
1223
1224    fn begin_metadata_attempt<R: krabka_protocol::ProtocolRequest>(&self) {
1225        if R::API_KEY != metadata_request::MetadataRequest::API_KEY
1226            || self.strategy == MetadataRecoveryStrategy::None
1227        {
1228            return;
1229        }
1230        self.first_metadata_attempt
1231            .lock()
1232            .unwrap_or_else(PoisonError::into_inner)
1233            .get_or_insert_with(Instant::now);
1234    }
1235
1236    fn response_requires_rebootstrap<T: Any>(response: &T) -> bool {
1237        (response as &dyn Any)
1238            .downcast_ref::<metadata_response::MetadataResponse>()
1239            .is_some_and(|metadata| metadata.error_code == 129)
1240    }
1241
1242    fn metadata_attempt_timed_out<T: Any>(&self, response: &T) -> bool {
1243        if self.strategy == MetadataRecoveryStrategy::None {
1244            return false;
1245        }
1246        let Some(metadata) =
1247            (response as &dyn Any).downcast_ref::<metadata_response::MetadataResponse>()
1248        else {
1249            return false;
1250        };
1251        metadata.brokers.is_empty()
1252            && self
1253                .first_metadata_attempt
1254                .lock()
1255                .unwrap_or_else(PoisonError::into_inner)
1256                .is_some_and(|started| started.elapsed() >= self.trigger.time().to_std())
1257    }
1258
1259    fn complete_metadata_attempt<T: Any>(&self, response: &T) {
1260        let has_brokers = (response as &dyn Any)
1261            .downcast_ref::<metadata_response::MetadataResponse>()
1262            .is_some_and(|metadata| !metadata.brokers.is_empty());
1263        if has_brokers {
1264            self.clear_metadata_attempt();
1265        }
1266    }
1267
1268    fn clear_metadata_attempt(&self) {
1269        *self
1270            .first_metadata_attempt
1271            .lock()
1272            .unwrap_or_else(PoisonError::into_inner) = None;
1273    }
1274}
1275
1276async fn send_connection_at_least<R>(
1277    connection: &Connection,
1278    request: R,
1279    min_version: i16,
1280) -> Result<R::Response, ClientError>
1281where
1282    R: krabka_protocol::ProtocolRequest,
1283{
1284    let (broker_min, broker_max) = connection
1285        .advertised_api_range(R::API_KEY)
1286        .unwrap_or((0, 0));
1287    let client_min = R::MIN_VERSION.max(min_version);
1288    let chosen = R::LATEST_STABLE_VERSION.min(broker_max);
1289    if chosen < client_min || chosen < broker_min {
1290        return Err(ClientError::IncompatibleVersion {
1291            api_key: R::API_KEY,
1292            broker_min,
1293            broker_max,
1294            client_min,
1295            client_max: R::LATEST_STABLE_VERSION,
1296        });
1297    }
1298    connection.send(request).await
1299}
1300
1301impl AdminClient {
1302    /// The connection options of Kafka's admin client defaults with the
1303    /// supplied security policy and a generated `adminclient-<n>` client id.
1304    fn opts(security: Option<security::ClientSecurity>) -> Result<ConnectionOptions, AdminError> {
1305        Ok(AdminClientConfig {
1306            security,
1307            ..AdminClientConfig::default()
1308        }
1309        .resolve()?
1310        .connection)
1311    }
1312
1313    /// Connects with Kafka's `AdminClientConfig` settings. Each call of the
1314    /// client uses the call deadline, retry backoff and retry count of
1315    /// `config`.
1316    ///
1317    /// # Errors
1318    /// Returns [`AdminError::InvalidConfig`] for invalid settings, and
1319    /// [`AdminError::Connect`] with the last cause if no bootstrap address
1320    /// connects.
1321    pub async fn connect_with_config(
1322        bootstrap_addrs: &[String],
1323        config: AdminClientConfig,
1324    ) -> Result<Self, AdminError> {
1325        let resolved = config.resolve()?;
1326        Self::connect_with_metadata_recovery_target(
1327            bootstrap_addrs,
1328            resolved.connection,
1329            resolved.retry,
1330            (
1331                resolved.metadata_recovery_strategy,
1332                resolved.metadata_recovery_rebootstrap_trigger,
1333            ),
1334            BootstrapTarget::Brokers,
1335            resolved.enable_metrics_push,
1336        )
1337        .await
1338    }
1339
1340    /// Controller-bootstrap variant of [`Self::connect_with_config`]
1341    /// (KIP-919).
1342    ///
1343    /// # Errors
1344    /// Returns [`AdminError::InvalidConfig`] for invalid settings, or a
1345    /// connection, protocol, or broker error when no controller bootstrap
1346    /// endpoint can identify and connect to the active controller.
1347    pub async fn connect_controller_with_config(
1348        bootstrap_controllers: &[String],
1349        config: AdminClientConfig,
1350    ) -> Result<Self, AdminError> {
1351        let resolved = config.resolve()?;
1352        Self::connect_with_metadata_recovery_target(
1353            bootstrap_controllers,
1354            resolved.connection,
1355            resolved.retry,
1356            (
1357                resolved.metadata_recovery_strategy,
1358                resolved.metadata_recovery_rebootstrap_trigger,
1359            ),
1360            BootstrapTarget::Controllers,
1361            resolved.enable_metrics_push,
1362        )
1363        .await
1364    }
1365
1366    /// Connects and applies optional client security. `None` means plaintext,
1367    /// which is identical to [`AdminClient::connect`].
1368    ///
1369    /// # Errors
1370    /// Returns [`AdminError::Connect`] if no bootstrap address accepted the
1371    /// (optionally secured) connection. Its `source` holds the last cause.
1372    pub async fn connect_secured(
1373        bootstrap_addrs: &[String],
1374        security: Option<security::ClientSecurity>,
1375    ) -> Result<Self, AdminError> {
1376        Self::connect_with_options(bootstrap_addrs, Self::opts(security)?).await
1377    }
1378
1379    /// Connects with the supplied security and DNS deadline, and keeps the
1380    /// standard admin identity, TCP-connect timeout, and request timeout.
1381    ///
1382    /// # Errors
1383    /// Returns [`AdminError::Connect`] with the last cause if no bootstrap
1384    /// address connects.
1385    pub async fn connect_secured_with_dns_timeout(
1386        bootstrap_addrs: &[String],
1387        security: Option<security::ClientSecurity>,
1388        dns_timeout: krabka_client_core::ClientDnsTimeout,
1389    ) -> Result<Self, AdminError> {
1390        let mut options = Self::opts(security)?;
1391        options.dns_timeout = dns_timeout;
1392        Self::connect_with_options(bootstrap_addrs, options).await
1393    }
1394
1395    /// Connects with the standard plaintext admin policy and a custom DNS
1396    /// deadline.
1397    ///
1398    /// # Errors
1399    /// Returns [`AdminError::Connect`] with the last cause if no bootstrap
1400    /// address connects.
1401    pub async fn connect_with_dns_timeout(
1402        bootstrap_addrs: &[String],
1403        dns_timeout: krabka_client_core::ClientDnsTimeout,
1404    ) -> Result<Self, AdminError> {
1405        Self::connect_secured_with_dns_timeout(bootstrap_addrs, None, dns_timeout).await
1406    }
1407
1408    /// Connects with a complete connection-options template.
1409    ///
1410    /// # Errors
1411    /// Returns [`AdminError::Connect`] with the last cause if no bootstrap
1412    /// address accepted the connection.
1413    pub async fn connect_with_options(
1414        bootstrap_addrs: &[String],
1415        options: ConnectionOptions,
1416    ) -> Result<Self, AdminError> {
1417        let retry = config::retry_for_connection_options(&options);
1418        Self::connect_with_metadata_recovery_target(
1419            bootstrap_addrs,
1420            options,
1421            retry,
1422            (
1423                MetadataRecoveryStrategy::default(),
1424                krabka_client_core::DEFAULT_METADATA_RECOVERY_REBOOTSTRAP_TRIGGER,
1425            ),
1426            BootstrapTarget::Brokers,
1427            DEFAULT_ENABLE_METRICS_PUSH,
1428        )
1429        .await
1430    }
1431
1432    /// Connects through KIP-919 controller bootstrap addresses rather than
1433    /// broker bootstrap addresses.
1434    ///
1435    /// # Errors
1436    /// Returns a connection, protocol, or broker error when no controller
1437    /// bootstrap endpoint can identify and connect to the active controller.
1438    pub async fn connect_controller(bootstrap_controllers: &[String]) -> Result<Self, AdminError> {
1439        Self::connect_controller_secured(bootstrap_controllers, None).await
1440    }
1441
1442    /// Controller-bootstrap variant of [`Self::connect_secured`].
1443    ///
1444    /// # Errors
1445    /// Returns a connection, protocol, or broker error when no controller
1446    /// bootstrap endpoint can identify and connect to the active controller.
1447    pub async fn connect_controller_secured(
1448        bootstrap_controllers: &[String],
1449        security: Option<security::ClientSecurity>,
1450    ) -> Result<Self, AdminError> {
1451        Self::connect_controller_with_options(bootstrap_controllers, Self::opts(security)?).await
1452    }
1453
1454    /// Controller-bootstrap variant of [`Self::connect_with_options`].
1455    ///
1456    /// # Errors
1457    /// Returns a connection, protocol, or broker error when no controller
1458    /// bootstrap endpoint can identify and connect to the active controller.
1459    pub async fn connect_controller_with_options(
1460        bootstrap_controllers: &[String],
1461        options: ConnectionOptions,
1462    ) -> Result<Self, AdminError> {
1463        let retry = config::retry_for_connection_options(&options);
1464        Self::connect_with_metadata_recovery_target(
1465            bootstrap_controllers,
1466            options,
1467            retry,
1468            (
1469                MetadataRecoveryStrategy::default(),
1470                krabka_client_core::DEFAULT_METADATA_RECOVERY_REBOOTSTRAP_TRIGGER,
1471            ),
1472            BootstrapTarget::Controllers,
1473            DEFAULT_ENABLE_METRICS_PUSH,
1474        )
1475        .await
1476    }
1477
1478    /// Connect with explicit KIP-1102 metadata recovery settings.
1479    ///
1480    /// # Errors
1481    /// Returns an invalid-configuration error for a negative or fractional
1482    /// millisecond trigger, or a connection error when no bootstrap endpoint
1483    /// is reachable.
1484    pub async fn connect_with_metadata_recovery(
1485        bootstrap_addrs: &[String],
1486        options: ConnectionOptions,
1487        strategy: MetadataRecoveryStrategy,
1488        rebootstrap_trigger: Time,
1489    ) -> Result<Self, AdminError> {
1490        let retry = config::retry_for_connection_options(&options);
1491        Self::connect_with_metadata_recovery_target(
1492            bootstrap_addrs,
1493            options,
1494            retry,
1495            (strategy, rebootstrap_trigger),
1496            BootstrapTarget::Brokers,
1497            DEFAULT_ENABLE_METRICS_PUSH,
1498        )
1499        .await
1500    }
1501
1502    async fn connect_with_metadata_recovery_target(
1503        bootstrap_addrs: &[String],
1504        mut options: ConnectionOptions,
1505        retry: retry::RetryPolicy,
1506        (strategy, rebootstrap_trigger): (MetadataRecoveryStrategy, Time),
1507        target: BootstrapTarget,
1508        enable_metrics_push: bool,
1509    ) -> Result<Self, AdminError> {
1510        let trigger = MetadataRecoveryRebootstrapTrigger::new(rebootstrap_trigger)
1511            .map_err(AdminError::Protocol)?;
1512        // Kafka's `CommonClientConfigs.telemetryReporter`: a reporter only
1513        // with `enable.metrics.push`. Every connection of the client counts
1514        // in its network metrics.
1515        let metrics = enable_metrics_push.then(ClientMetrics::new);
1516        options.network_metrics = metrics.as_ref().map(|metrics| {
1517            NetworkMetrics::register(metrics, TelemetryClientType::Admin.metric_group())
1518        });
1519        let mut last_error = None;
1520        for host_port in bootstrap_addrs {
1521            match Self::connect_target_one_with_discovery(host_port, options.clone(), target).await
1522            {
1523                Ok(connected) => {
1524                    let known_addrs = if connected.discovered_addrs.is_empty() {
1525                        bootstrap_addrs.to_vec()
1526                    } else {
1527                        connected.discovered_addrs
1528                    };
1529                    let conn = Arc::new(RecoveringConnection::new(
1530                        connected.connection,
1531                        bootstrap_addrs.to_vec(),
1532                        options.clone(),
1533                        strategy,
1534                        trigger,
1535                        target,
1536                        known_addrs,
1537                    ));
1538                    let telemetry = metrics.map(|metrics| {
1539                        ClientTelemetry::start(
1540                            telemetry::AdminTelemetryConnection(Arc::clone(&conn)),
1541                            &ClientTelemetryConfig::admin(),
1542                            metrics,
1543                            TelemetryTiming {
1544                                request_timeout: options.request_timeout.to_std(),
1545                                reconnect_backoff: options.reconnect_backoff.to_std(),
1546                            },
1547                        )
1548                    });
1549                    return Ok(Self {
1550                        conn,
1551                        bootstrap_addrs: bootstrap_addrs.to_vec(),
1552                        options,
1553                        retry,
1554                        telemetry,
1555                    });
1556                }
1557                Err(e) if e.is_authentication_failure() => return Err(e),
1558                Err(e) => {
1559                    tracing::debug!(
1560                        target: "krabka_client_admin",
1561                        addr = %host_port,
1562                        error = %e,
1563                        "bootstrap connect failed",
1564                    );
1565                    last_error = Some(e);
1566                }
1567            }
1568        }
1569        match (target, last_error) {
1570            (BootstrapTarget::Controllers, Some(error)) => Err(error),
1571            (_, last_error) => Err(AdminError::Connect {
1572                tried: bootstrap_addrs.len(),
1573                source: last_error.map(Box::new),
1574            }),
1575        }
1576    }
1577
1578    /// Tries each bootstrap address in order.
1579    ///
1580    /// Each entry is `host:port`, which
1581    /// [`krabka_client_core::transport::resolve`] resolves.
1582    /// The first successful connect wins. Returns
1583    /// [`AdminError::Connect`] with the last cause if none responded. The connection is
1584    /// plaintext. See [`AdminClient::connect_secured`].
1585    ///
1586    /// # Errors
1587    /// Returns an error when configuration is invalid, protocol encoding fails, the broker rejects the request, or transport I/O fails.
1588    pub async fn connect(bootstrap_addrs: &[String]) -> Result<Self, AdminError> {
1589        Self::connect_secured(bootstrap_addrs, None).await
1590    }
1591
1592    async fn connect_one(
1593        host_port: &str,
1594        mut opts: ConnectionOptions,
1595    ) -> Result<Connection, AdminError> {
1596        if let Some(security) = opts.security.as_mut() {
1597            **security = security.for_target_host(connection_target_host(host_port));
1598        }
1599        let addr = lookup_first(host_port, opts.dns_timeout, transport::resolve(host_port)).await?;
1600        Connection::connect_with_options(addr, opts)
1601            .await
1602            .map_err(AdminError::from)
1603    }
1604
1605    #[cfg(test)]
1606    async fn connect_target_one(
1607        host_port: &str,
1608        options: ConnectionOptions,
1609        target: BootstrapTarget,
1610    ) -> Result<Connection, AdminError> {
1611        Self::connect_target_one_with_discovery(host_port, options, target)
1612            .await
1613            .map(|connected| connected.connection)
1614    }
1615
1616    async fn connect_target_one_with_discovery(
1617        host_port: &str,
1618        options: ConnectionOptions,
1619        target: BootstrapTarget,
1620    ) -> Result<ConnectedTarget, AdminError> {
1621        match target {
1622            BootstrapTarget::Brokers => Ok(ConnectedTarget {
1623                connection: Self::connect_one(host_port, options).await?,
1624                discovered_addrs: Vec::new(),
1625            }),
1626            BootstrapTarget::Controllers => {
1627                Self::connect_controller_one_with_discovery(host_port, options).await
1628            }
1629        }
1630    }
1631
1632    async fn connect_controller_one_with_discovery(
1633        host_port: &str,
1634        options: ConnectionOptions,
1635    ) -> Result<ConnectedTarget, AdminError> {
1636        use krabka_protocol::owned::describe_cluster_request::DescribeClusterRequest;
1637
1638        const ENDPOINT_TYPE_CONTROLLERS: i8 = 2;
1639        let bootstrap = Self::connect_one(host_port, options.clone()).await?;
1640        let response = send_connection_at_least(
1641            &bootstrap,
1642            DescribeClusterRequest {
1643                endpoint_type: ENDPOINT_TYPE_CONTROLLERS,
1644                ..Default::default()
1645            },
1646            1,
1647        )
1648        .await?;
1649        if response.error_code != 0 {
1650            return Err(AdminError::Broker {
1651                api: "DescribeCluster",
1652                code: response.error_code,
1653                name: kafka_error_name(response.error_code),
1654                message: response.error_message,
1655            });
1656        }
1657        if response.endpoint_type != ENDPOINT_TYPE_CONTROLLERS {
1658            return Err(AdminError::Protocol(format!(
1659                "DescribeCluster returned endpoint_type={}, expected CONTROLLERS",
1660                response.endpoint_type
1661            )));
1662        }
1663        let discovered_addrs = response
1664            .brokers
1665            .iter()
1666            .filter(|controller| !controller.host.is_empty() && controller.port > 0)
1667            .map(|controller| format_host_port(&controller.host, controller.port))
1668            .collect::<Vec<_>>();
1669        let controller = response
1670            .brokers
1671            .into_iter()
1672            .find(|controller| controller.broker_id == response.controller_id)
1673            .ok_or_else(|| {
1674                AdminError::Protocol(format!(
1675                    "DescribeCluster omitted controller_id={} from controller endpoints",
1676                    response.controller_id
1677                ))
1678            })?;
1679        if controller.host.is_empty() || controller.port <= 0 {
1680            return Err(AdminError::Protocol(format!(
1681                "DescribeCluster returned invalid controller endpoint {}:{}",
1682                controller.host, controller.port
1683            )));
1684        }
1685        let endpoint = format_host_port(&controller.host, controller.port);
1686        Ok(ConnectedTarget {
1687            connection: Self::connect_one(&endpoint, options).await?,
1688            discovered_addrs,
1689        })
1690    }
1691
1692    /// Replaces the underlying connection. The `NOT_CONTROLLER` retry path
1693    /// uses it internally to reconnect to the current controller.
1694    pub(crate) async fn reconnect(&self, host_port: &str) -> Result<(), AdminError> {
1695        let opts = self.options.clone();
1696        self.conn
1697            .replace(Self::connect_one(host_port, opts).await?)
1698            .await;
1699        Ok(())
1700    }
1701
1702    #[cfg(test)]
1703    pub(crate) async fn reconnect_bootstrap(&self) -> Result<(), AdminError> {
1704        let opts = self.options.clone();
1705        let mut last_error = None;
1706        for host_port in &self.bootstrap_addrs {
1707            match Self::connect_target_one(host_port, opts.clone(), self.conn.target).await {
1708                Ok(conn) => {
1709                    self.conn.replace(conn).await;
1710                    return Ok(());
1711                }
1712                Err(error) if error.is_authentication_failure() => return Err(error),
1713                Err(error) => {
1714                    tracing::debug!(
1715                        target: "krabka_client_admin",
1716                        addr = %host_port,
1717                        error = %error,
1718                        "bootstrap reconnect failed",
1719                    );
1720                    last_error = Some(Box::new(error));
1721                }
1722            }
1723        }
1724        Err(AdminError::Connect {
1725            tried: self.bootstrap_addrs.len(),
1726            source: last_error,
1727        })
1728    }
1729
1730    pub(crate) fn is_retriable_transport_error(error: &ClientError) -> bool {
1731        matches!(
1732            error,
1733            ClientError::Timeout(_) | ClientError::Disconnected | ClientError::Io(_)
1734        )
1735    }
1736}
1737
1738/// Kafka error code: the broker is not the controller (KIP-129). The
1739/// admin client refreshes its controller endpoint and retries until the call
1740/// deadline.
1741pub(crate) const NOT_CONTROLLER: i16 = 41;
1742
1743/// Maps a Kafka error code to the name of its `Errors` constant, for
1744/// human-friendly `AdminError::Broker` output.
1745///
1746/// The table holds every code of Apache Kafka's
1747/// `org.apache.kafka.common.protocol.Errors` enum at apache/kafka@3eb0b8c.
1748/// A code that Kafka does not define gives `"UNKNOWN"`.
1749pub(crate) fn kafka_error_name(code: i16) -> &'static str {
1750    match code {
1751        -1 => "UNKNOWN_SERVER_ERROR",
1752        0 => "NONE",
1753        1 => "OFFSET_OUT_OF_RANGE",
1754        2 => "CORRUPT_MESSAGE",
1755        3 => "UNKNOWN_TOPIC_OR_PARTITION",
1756        4 => "INVALID_FETCH_SIZE",
1757        5 => "LEADER_NOT_AVAILABLE",
1758        6 => "NOT_LEADER_OR_FOLLOWER",
1759        7 => "REQUEST_TIMED_OUT",
1760        8 => "BROKER_NOT_AVAILABLE",
1761        9 => "REPLICA_NOT_AVAILABLE",
1762        10 => "MESSAGE_TOO_LARGE",
1763        11 => "STALE_CONTROLLER_EPOCH",
1764        12 => "OFFSET_METADATA_TOO_LARGE",
1765        13 => "NETWORK_EXCEPTION",
1766        14 => "COORDINATOR_LOAD_IN_PROGRESS",
1767        15 => "COORDINATOR_NOT_AVAILABLE",
1768        16 => "NOT_COORDINATOR",
1769        17 => "INVALID_TOPIC_EXCEPTION",
1770        18 => "RECORD_LIST_TOO_LARGE",
1771        19 => "NOT_ENOUGH_REPLICAS",
1772        20 => "NOT_ENOUGH_REPLICAS_AFTER_APPEND",
1773        21 => "INVALID_REQUIRED_ACKS",
1774        22 => "ILLEGAL_GENERATION",
1775        23 => "INCONSISTENT_GROUP_PROTOCOL",
1776        24 => "INVALID_GROUP_ID",
1777        25 => "UNKNOWN_MEMBER_ID",
1778        26 => "INVALID_SESSION_TIMEOUT",
1779        27 => "REBALANCE_IN_PROGRESS",
1780        28 => "INVALID_COMMIT_OFFSET_SIZE",
1781        29 => "TOPIC_AUTHORIZATION_FAILED",
1782        30 => "GROUP_AUTHORIZATION_FAILED",
1783        31 => "CLUSTER_AUTHORIZATION_FAILED",
1784        32 => "INVALID_TIMESTAMP",
1785        33 => "UNSUPPORTED_SASL_MECHANISM",
1786        34 => "ILLEGAL_SASL_STATE",
1787        35 => "UNSUPPORTED_VERSION",
1788        36 => "TOPIC_ALREADY_EXISTS",
1789        37 => "INVALID_PARTITIONS",
1790        38 => "INVALID_REPLICATION_FACTOR",
1791        39 => "INVALID_REPLICA_ASSIGNMENT",
1792        40 => "INVALID_CONFIG",
1793        41 => "NOT_CONTROLLER",
1794        42 => "INVALID_REQUEST",
1795        43 => "UNSUPPORTED_FOR_MESSAGE_FORMAT",
1796        44 => "POLICY_VIOLATION",
1797        45 => "OUT_OF_ORDER_SEQUENCE_NUMBER",
1798        46 => "DUPLICATE_SEQUENCE_NUMBER",
1799        47 => "INVALID_PRODUCER_EPOCH",
1800        48 => "INVALID_TXN_STATE",
1801        49 => "INVALID_PRODUCER_ID_MAPPING",
1802        50 => "INVALID_TRANSACTION_TIMEOUT",
1803        51 => "CONCURRENT_TRANSACTIONS",
1804        52 => "TRANSACTION_COORDINATOR_FENCED",
1805        53 => "TRANSACTIONAL_ID_AUTHORIZATION_FAILED",
1806        54 => "SECURITY_DISABLED",
1807        55 => "OPERATION_NOT_ATTEMPTED",
1808        56 => "KAFKA_STORAGE_ERROR",
1809        57 => "LOG_DIR_NOT_FOUND",
1810        58 => "SASL_AUTHENTICATION_FAILED",
1811        59 => "UNKNOWN_PRODUCER_ID",
1812        60 => "REASSIGNMENT_IN_PROGRESS",
1813        61 => "DELEGATION_TOKEN_AUTH_DISABLED",
1814        62 => "DELEGATION_TOKEN_NOT_FOUND",
1815        63 => "DELEGATION_TOKEN_OWNER_MISMATCH",
1816        64 => "DELEGATION_TOKEN_REQUEST_NOT_ALLOWED",
1817        65 => "DELEGATION_TOKEN_AUTHORIZATION_FAILED",
1818        66 => "DELEGATION_TOKEN_EXPIRED",
1819        67 => "INVALID_PRINCIPAL_TYPE",
1820        68 => "NON_EMPTY_GROUP",
1821        69 => "GROUP_ID_NOT_FOUND",
1822        70 => "FETCH_SESSION_ID_NOT_FOUND",
1823        71 => "INVALID_FETCH_SESSION_EPOCH",
1824        72 => "LISTENER_NOT_FOUND",
1825        73 => "TOPIC_DELETION_DISABLED",
1826        74 => "FENCED_LEADER_EPOCH",
1827        75 => "UNKNOWN_LEADER_EPOCH",
1828        76 => "UNSUPPORTED_COMPRESSION_TYPE",
1829        77 => "STALE_BROKER_EPOCH",
1830        78 => "OFFSET_NOT_AVAILABLE",
1831        79 => "MEMBER_ID_REQUIRED",
1832        80 => "PREFERRED_LEADER_NOT_AVAILABLE",
1833        81 => "GROUP_MAX_SIZE_REACHED",
1834        82 => "FENCED_INSTANCE_ID",
1835        83 => "ELIGIBLE_LEADERS_NOT_AVAILABLE",
1836        84 => "ELECTION_NOT_NEEDED",
1837        85 => "NO_REASSIGNMENT_IN_PROGRESS",
1838        86 => "GROUP_SUBSCRIBED_TO_TOPIC",
1839        87 => "INVALID_RECORD",
1840        88 => "UNSTABLE_OFFSET_COMMIT",
1841        89 => "THROTTLING_QUOTA_EXCEEDED",
1842        90 => "PRODUCER_FENCED",
1843        91 => "RESOURCE_NOT_FOUND",
1844        92 => "DUPLICATE_RESOURCE",
1845        93 => "UNACCEPTABLE_CREDENTIAL",
1846        94 => "INCONSISTENT_VOTER_SET",
1847        95 => "INVALID_UPDATE_VERSION",
1848        96 => "FEATURE_UPDATE_FAILED",
1849        97 => "PRINCIPAL_DESERIALIZATION_FAILURE",
1850        98 => "SNAPSHOT_NOT_FOUND",
1851        99 => "POSITION_OUT_OF_RANGE",
1852        100 => "UNKNOWN_TOPIC_ID",
1853        101 => "DUPLICATE_BROKER_REGISTRATION",
1854        102 => "BROKER_ID_NOT_REGISTERED",
1855        103 => "INCONSISTENT_TOPIC_ID",
1856        104 => "INCONSISTENT_CLUSTER_ID",
1857        105 => "TRANSACTIONAL_ID_NOT_FOUND",
1858        106 => "FETCH_SESSION_TOPIC_ID_ERROR",
1859        107 => "INELIGIBLE_REPLICA",
1860        108 => "NEW_LEADER_ELECTED",
1861        109 => "OFFSET_MOVED_TO_TIERED_STORAGE",
1862        110 => "FENCED_MEMBER_EPOCH",
1863        111 => "UNRELEASED_INSTANCE_ID",
1864        112 => "UNSUPPORTED_ASSIGNOR",
1865        113 => "STALE_MEMBER_EPOCH",
1866        114 => "MISMATCHED_ENDPOINT_TYPE",
1867        115 => "UNSUPPORTED_ENDPOINT_TYPE",
1868        116 => "UNKNOWN_CONTROLLER_ID",
1869        117 => "UNKNOWN_SUBSCRIPTION_ID",
1870        118 => "TELEMETRY_TOO_LARGE",
1871        119 => "INVALID_REGISTRATION",
1872        120 => "TRANSACTION_ABORTABLE",
1873        121 => "INVALID_RECORD_STATE",
1874        122 => "SHARE_SESSION_NOT_FOUND",
1875        123 => "INVALID_SHARE_SESSION_EPOCH",
1876        124 => "FENCED_STATE_EPOCH",
1877        125 => "INVALID_VOTER_KEY",
1878        126 => "DUPLICATE_VOTER",
1879        127 => "VOTER_NOT_FOUND",
1880        128 => "INVALID_REGULAR_EXPRESSION",
1881        129 => "REBOOTSTRAP_REQUIRED",
1882        130 => "STREAMS_INVALID_TOPOLOGY",
1883        131 => "STREAMS_INVALID_TOPOLOGY_EPOCH",
1884        132 => "STREAMS_TOPOLOGY_FENCED",
1885        133 => "SHARE_SESSION_LIMIT_REACHED",
1886        134 => "GROUP_DELETION_FAILED",
1887        135 => "STREAMS_TOPOLOGY_DESCRIPTION_UPDATE_FAILED",
1888        136 => "CONTROLLER_ID_NOT_REGISTERED",
1889        _ => "UNKNOWN",
1890    }
1891}
1892
1893#[cfg(test)]
1894mod tests {
1895    use std::{
1896        future::pending,
1897        hint,
1898        sync::{
1899            Arc, Mutex,
1900            atomic::{AtomicU16, AtomicUsize, Ordering},
1901        },
1902        time::Duration,
1903        vec,
1904    };
1905
1906    use bytes::{BufMut, BytesMut};
1907    use krabka_protocol::{
1908        Decode, Encode,
1909        owned::{
1910            api_versions_request,
1911            api_versions_response::{ApiVersion, ApiVersionsResponse},
1912            describe_cluster_request::{self, DescribeClusterRequest},
1913            describe_cluster_response::{DescribeClusterBroker, DescribeClusterResponse},
1914            metadata_request,
1915            metadata_response::{
1916                FLEXIBLE_MIN as METADATA_FLEXIBLE_MIN, MetadataResponse, MetadataResponseBroker,
1917            },
1918            sasl_authenticate_request,
1919            sasl_authenticate_response::SaslAuthenticateResponse,
1920            sasl_handshake_request,
1921            sasl_handshake_response::SaslHandshakeResponse,
1922        },
1923    };
1924    use krabka_security::ListenerProtocol;
1925    use krabka_units::secs;
1926    use security::{ClientSecurity, SaslCredentials};
1927    use tokio::{
1928        io::{AsyncReadExt, AsyncWriteExt},
1929        net::{TcpListener, TcpStream},
1930        task::yield_now,
1931        time::sleep,
1932    };
1933    use tokio_util::sync::CancellationToken;
1934
1935    use super::*;
1936
1937    struct ObservedAdminBroker {
1938        addr: SocketAddr,
1939        shutdown: CancellationToken,
1940        connections: Arc<AtomicUsize>,
1941        sasl_handshakes: Arc<AtomicUsize>,
1942        client_ids: Arc<Mutex<Vec<String>>>,
1943    }
1944
1945    impl ObservedAdminBroker {
1946        async fn start(api_versions_delay: Duration) -> Self {
1947            let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1948            let addr = listener.local_addr().unwrap();
1949            let shutdown = CancellationToken::new();
1950            let connections = Arc::new(AtomicUsize::new(0));
1951            let sasl_handshakes = Arc::new(AtomicUsize::new(0));
1952            let client_ids = Arc::new(Mutex::new(Vec::new()));
1953            let task_shutdown = shutdown.clone();
1954            let task_connections = Arc::clone(&connections);
1955            let task_sasl_handshakes = Arc::clone(&sasl_handshakes);
1956            let task_client_ids = Arc::clone(&client_ids);
1957
1958            tokio::spawn(async move {
1959                loop {
1960                    let Ok((mut stream, _)) = listener.accept().await else {
1961                        break;
1962                    };
1963                    if task_shutdown.is_cancelled() {
1964                        break;
1965                    }
1966                    task_connections.fetch_add(1, Ordering::SeqCst);
1967                    let conn_sasl_handshakes = Arc::clone(&task_sasl_handshakes);
1968                    let conn_client_ids = Arc::clone(&task_client_ids);
1969                    tokio::spawn(async move {
1970                        while let Ok(frame_len) = stream.read_u32().await {
1971                            let mut request = vec![0_u8; frame_len as usize];
1972                            if stream.read_exact(&mut request).await.is_err() || request.len() < 10
1973                            {
1974                                break;
1975                            }
1976                            let api_key = i16::from_be_bytes([request[0], request[1]]);
1977                            let correlation_id =
1978                                i32::from_be_bytes(request[4..8].try_into().unwrap());
1979                            let client_id_len =
1980                                usize::from(u16::from_be_bytes([request[8], request[9]]));
1981                            if request.len() < 10 + client_id_len {
1982                                break;
1983                            }
1984                            conn_client_ids.lock().unwrap().push(
1985                                String::from_utf8_lossy(&request[10..10 + client_id_len]).into(),
1986                            );
1987
1988                            let (body, flexible_header) = match api_key {
1989                                sasl_handshake_request::API_KEY => {
1990                                    conn_sasl_handshakes.fetch_add(1, Ordering::SeqCst);
1991                                    let mut body = BytesMut::new();
1992                                    SaslHandshakeResponse {
1993                                        error_code: 0,
1994                                        ..Default::default()
1995                                    }
1996                                    .encode(&mut body, 1)
1997                                    .unwrap();
1998                                    (body, false)
1999                                }
2000                                sasl_authenticate_request::API_KEY => {
2001                                    let mut body = BytesMut::new();
2002                                    SaslAuthenticateResponse {
2003                                        error_code: 0,
2004                                        ..Default::default()
2005                                    }
2006                                    .encode(&mut body, 2)
2007                                    .unwrap();
2008                                    (body, true)
2009                                }
2010                                api_versions_request::API_KEY => {
2011                                    sleep(api_versions_delay).await;
2012                                    let mut body = BytesMut::new();
2013                                    ApiVersionsResponse {
2014                                        error_code: 0,
2015                                        api_keys: vec![
2016                                            ApiVersion {
2017                                                api_key: api_versions_request::API_KEY,
2018                                                min_version: 0,
2019                                                max_version: 3,
2020                                                ..Default::default()
2021                                            },
2022                                            ApiVersion {
2023                                                api_key: metadata_request::API_KEY,
2024                                                min_version: 0,
2025                                                max_version: 12,
2026                                                ..Default::default()
2027                                            },
2028                                            ApiVersion {
2029                                                api_key: sasl_handshake_request::API_KEY,
2030                                                min_version: 0,
2031                                                max_version: 1,
2032                                                ..Default::default()
2033                                            },
2034                                            ApiVersion {
2035                                                api_key: sasl_authenticate_request::API_KEY,
2036                                                min_version: 0,
2037                                                max_version: 2,
2038                                                ..Default::default()
2039                                            },
2040                                        ],
2041                                        ..Default::default()
2042                                    }
2043                                    .encode(&mut body, 0)
2044                                    .unwrap();
2045                                    (body, false)
2046                                }
2047                                _ => continue,
2048                            };
2049                            let mut response = BytesMut::new();
2050                            response.put_i32(correlation_id);
2051                            if flexible_header {
2052                                response.put_u8(0);
2053                            }
2054                            response.extend_from_slice(&body);
2055                            if stream
2056                                .write_u32(u32::try_from(response.len()).unwrap())
2057                                .await
2058                                .is_err()
2059                                || stream.write_all(&response).await.is_err()
2060                            {
2061                                break;
2062                            }
2063                        }
2064                    });
2065                }
2066            });
2067
2068            Self {
2069                addr,
2070                shutdown,
2071                connections,
2072                sasl_handshakes,
2073                client_ids,
2074            }
2075        }
2076
2077        fn observed_custom_security_and_id(&self) -> bool {
2078            self.sasl_handshakes.load(Ordering::SeqCst) > 0
2079                && self
2080                    .client_ids
2081                    .lock()
2082                    .unwrap()
2083                    .iter()
2084                    .any(|client_id| client_id == "custom-admin")
2085        }
2086
2087        fn stop(self) {
2088            self.shutdown.cancel();
2089        }
2090    }
2091
2092    struct ObservedController {
2093        addr: SocketAddr,
2094        connections: Arc<AtomicUsize>,
2095        endpoint_types: Arc<Mutex<Vec<i8>>>,
2096    }
2097
2098    impl ObservedController {
2099        async fn start(describe_cluster_max_version: i16) -> Self {
2100            let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2101            let addr = listener.local_addr().unwrap();
2102            let connections = Arc::new(AtomicUsize::new(0));
2103            let endpoint_types = Arc::new(Mutex::new(Vec::new()));
2104            let task_connections = Arc::clone(&connections);
2105            let task_endpoint_types = Arc::clone(&endpoint_types);
2106
2107            tokio::spawn(async move {
2108                while let Ok((mut stream, _)) = listener.accept().await {
2109                    task_connections.fetch_add(1, Ordering::SeqCst);
2110                    let endpoint_types = Arc::clone(&task_endpoint_types);
2111                    tokio::spawn(async move {
2112                        while let Ok(frame_len) = stream.read_u32().await {
2113                            let mut request = vec![0_u8; frame_len as usize];
2114                            if stream.read_exact(&mut request).await.is_err() || request.len() < 10
2115                            {
2116                                break;
2117                            }
2118                            let api_key = i16::from_be_bytes([request[0], request[1]]);
2119                            let api_version = i16::from_be_bytes([request[2], request[3]]);
2120                            let correlation_id =
2121                                i32::from_be_bytes(request[4..8].try_into().unwrap());
2122                            let client_id_len =
2123                                usize::from(u16::from_be_bytes([request[8], request[9]]));
2124                            let header_len = 10 + client_id_len;
2125                            if request.len() < header_len {
2126                                break;
2127                            }
2128
2129                            let (body, flexible_header) = match api_key {
2130                                api_versions_request::API_KEY => {
2131                                    let mut body = BytesMut::new();
2132                                    ApiVersionsResponse {
2133                                        api_keys: vec![
2134                                            ApiVersion {
2135                                                api_key: api_versions_request::API_KEY,
2136                                                min_version: 0,
2137                                                max_version: 4,
2138                                                ..Default::default()
2139                                            },
2140                                            ApiVersion {
2141                                                api_key: describe_cluster_request::API_KEY,
2142                                                min_version: 0,
2143                                                max_version: describe_cluster_max_version,
2144                                                ..Default::default()
2145                                            },
2146                                        ],
2147                                        ..Default::default()
2148                                    }
2149                                    .encode(&mut body, 0)
2150                                    .unwrap();
2151                                    (body, false)
2152                                }
2153                                describe_cluster_request::API_KEY => {
2154                                    let mut encoded = &request[header_len + 1..];
2155                                    let decoded =
2156                                        DescribeClusterRequest::decode(&mut encoded, api_version)
2157                                            .unwrap();
2158                                    endpoint_types.lock().unwrap().push(decoded.endpoint_type);
2159                                    let mut body = BytesMut::new();
2160                                    DescribeClusterResponse {
2161                                        endpoint_type: 2,
2162                                        cluster_id: "controller-test".into(),
2163                                        controller_id: 1,
2164                                        brokers: vec![DescribeClusterBroker {
2165                                            broker_id: 1,
2166                                            host: addr.ip().to_string(),
2167                                            port: i32::from(addr.port()),
2168                                            ..Default::default()
2169                                        }],
2170                                        ..Default::default()
2171                                    }
2172                                    .encode(&mut body, api_version)
2173                                    .unwrap();
2174                                    (body, true)
2175                                }
2176                                _ => continue,
2177                            };
2178                            let mut response = BytesMut::new();
2179                            response.put_i32(correlation_id);
2180                            if flexible_header {
2181                                response.put_u8(0);
2182                            }
2183                            response.extend_from_slice(&body);
2184                            if stream
2185                                .write_u32(u32::try_from(response.len()).unwrap())
2186                                .await
2187                                .is_err()
2188                                || stream.write_all(&response).await.is_err()
2189                            {
2190                                break;
2191                            }
2192                        }
2193                    });
2194                }
2195            });
2196
2197            Self {
2198                addr,
2199                connections,
2200                endpoint_types,
2201            }
2202        }
2203    }
2204
2205    fn custom_admin_options() -> ConnectionOptions {
2206        ConnectionOptions {
2207            dns_timeout: ClientDnsTimeout::default(),
2208            client_id: "custom-admin".into(),
2209            socket_connection_setup_timeout: krabka_units::millis(100),
2210            request_timeout: krabka_units::millis(150),
2211            dispatch_queue_capacity: krabka_client_core::ConnectionDispatchQueueCapacity::new(7)
2212                .unwrap(),
2213            frame_max: krabka_client_core::ClientFrameMax::try_from(krabka_units::kibibytes(32))
2214                .unwrap(),
2215            security: Some(Box::new(ClientSecurity {
2216                protocol: ListenerProtocol::SaslPlaintext,
2217                tls: None,
2218                sasl: Some(SaslCredentials::Plain {
2219                    username: "u".into(),
2220                    password: "p".into(),
2221                }),
2222                sasl_host: Some("broker.example".into()),
2223            })),
2224            ..ConnectionOptions::default()
2225        }
2226    }
2227
2228    async fn metadata_times_out_with_custom_request_timeout(admin: &mut AdminClient) {
2229        let result = timeout(Duration::from_secs(2), admin.metadata(&[]))
2230            .await
2231            .expect("custom request timeout fires");
2232        assert2::assert!(result.is_err());
2233    }
2234
2235    fn assert_custom_connect_timeout_is_stored(admin: &AdminClient) {
2236        assert2::assert!(
2237            admin.options.socket_connection_setup_timeout == krabka_units::millis(100)
2238        );
2239        assert2::assert!(admin.options.dispatch_queue_capacity.get() == 7);
2240        assert2::assert!(admin.options.frame_max.size() == krabka_units::kibibytes(32));
2241    }
2242
2243    fn metadata_mock_response(version: i16, broker_id: i32, port: u16) -> Vec<u8> {
2244        let response = MetadataResponse {
2245            brokers: vec![MetadataResponseBroker {
2246                node_id: broker_id,
2247                host: "127.0.0.1".into(),
2248                port: i32::from(port),
2249                ..Default::default()
2250            }],
2251            ..Default::default()
2252        };
2253        let mut bytes = BytesMut::new();
2254        if version >= METADATA_FLEXIBLE_MIN {
2255            bytes.put_u8(0);
2256        }
2257        response.encode(&mut bytes, version).unwrap();
2258        bytes.to_vec()
2259    }
2260
2261    fn admin_mock_api_versions() -> Vec<u8> {
2262        let response = ApiVersionsResponse {
2263            api_keys: vec![
2264                ApiVersion {
2265                    api_key: api_versions_request::API_KEY,
2266                    min_version: 0,
2267                    max_version: 3,
2268                    ..Default::default()
2269                },
2270                ApiVersion {
2271                    api_key: metadata_request::API_KEY,
2272                    min_version: 0,
2273                    max_version: 13,
2274                    ..Default::default()
2275                },
2276            ],
2277            ..Default::default()
2278        };
2279        let mut bytes = BytesMut::new();
2280        response.encode(&mut bytes, 0).unwrap();
2281        bytes.to_vec()
2282    }
2283
2284    fn admin_mock_api_versions_with_describe_cluster(max_version: i16) -> Vec<u8> {
2285        let response = ApiVersionsResponse {
2286            api_keys: vec![
2287                ApiVersion {
2288                    api_key: api_versions_request::API_KEY,
2289                    min_version: 0,
2290                    max_version: 3,
2291                    ..Default::default()
2292                },
2293                ApiVersion {
2294                    api_key: describe_cluster_request::API_KEY,
2295                    min_version: 0,
2296                    max_version,
2297                    ..Default::default()
2298                },
2299            ],
2300            ..Default::default()
2301        };
2302        let mut bytes = BytesMut::new();
2303        response.encode(&mut bytes, 0).unwrap();
2304        bytes.to_vec()
2305    }
2306
2307    async fn wait_for_admin_mock_closed(addr: SocketAddr) {
2308        timeout(Duration::from_secs(10), async {
2309            while TcpStream::connect(addr).await.is_ok() {
2310                yield_now().await;
2311            }
2312        })
2313        .await
2314        .expect("mock broker listener closes");
2315    }
2316
2317    #[test]
2318    fn error_names_match_the_codes_that_apache_kafka_defines() {
2319        // Every constant of Apache Kafka's `Errors` enum at
2320        // apache/kafka@3eb0b8c, in code order, as `Errors.values()` prints
2321        // `code()` and `name()`. The list is exhaustive on purpose: a sampled
2322        // table lets a deleted arm pass unnoticed, and a wrong name sends an
2323        // operator to the wrong cause.
2324        let cases: [(i16, &str); 138] = [
2325            (-1, "UNKNOWN_SERVER_ERROR"),
2326            (0, "NONE"),
2327            (1, "OFFSET_OUT_OF_RANGE"),
2328            (2, "CORRUPT_MESSAGE"),
2329            (3, "UNKNOWN_TOPIC_OR_PARTITION"),
2330            (4, "INVALID_FETCH_SIZE"),
2331            (5, "LEADER_NOT_AVAILABLE"),
2332            (6, "NOT_LEADER_OR_FOLLOWER"),
2333            (7, "REQUEST_TIMED_OUT"),
2334            (8, "BROKER_NOT_AVAILABLE"),
2335            (9, "REPLICA_NOT_AVAILABLE"),
2336            (10, "MESSAGE_TOO_LARGE"),
2337            (11, "STALE_CONTROLLER_EPOCH"),
2338            (12, "OFFSET_METADATA_TOO_LARGE"),
2339            (13, "NETWORK_EXCEPTION"),
2340            (14, "COORDINATOR_LOAD_IN_PROGRESS"),
2341            (15, "COORDINATOR_NOT_AVAILABLE"),
2342            (16, "NOT_COORDINATOR"),
2343            (17, "INVALID_TOPIC_EXCEPTION"),
2344            (18, "RECORD_LIST_TOO_LARGE"),
2345            (19, "NOT_ENOUGH_REPLICAS"),
2346            (20, "NOT_ENOUGH_REPLICAS_AFTER_APPEND"),
2347            (21, "INVALID_REQUIRED_ACKS"),
2348            (22, "ILLEGAL_GENERATION"),
2349            (23, "INCONSISTENT_GROUP_PROTOCOL"),
2350            (24, "INVALID_GROUP_ID"),
2351            (25, "UNKNOWN_MEMBER_ID"),
2352            (26, "INVALID_SESSION_TIMEOUT"),
2353            (27, "REBALANCE_IN_PROGRESS"),
2354            (28, "INVALID_COMMIT_OFFSET_SIZE"),
2355            (29, "TOPIC_AUTHORIZATION_FAILED"),
2356            (30, "GROUP_AUTHORIZATION_FAILED"),
2357            (31, "CLUSTER_AUTHORIZATION_FAILED"),
2358            (32, "INVALID_TIMESTAMP"),
2359            (33, "UNSUPPORTED_SASL_MECHANISM"),
2360            (34, "ILLEGAL_SASL_STATE"),
2361            (35, "UNSUPPORTED_VERSION"),
2362            (36, "TOPIC_ALREADY_EXISTS"),
2363            (37, "INVALID_PARTITIONS"),
2364            (38, "INVALID_REPLICATION_FACTOR"),
2365            (39, "INVALID_REPLICA_ASSIGNMENT"),
2366            (40, "INVALID_CONFIG"),
2367            (41, "NOT_CONTROLLER"),
2368            (42, "INVALID_REQUEST"),
2369            (43, "UNSUPPORTED_FOR_MESSAGE_FORMAT"),
2370            (44, "POLICY_VIOLATION"),
2371            (45, "OUT_OF_ORDER_SEQUENCE_NUMBER"),
2372            (46, "DUPLICATE_SEQUENCE_NUMBER"),
2373            (47, "INVALID_PRODUCER_EPOCH"),
2374            (48, "INVALID_TXN_STATE"),
2375            (49, "INVALID_PRODUCER_ID_MAPPING"),
2376            (50, "INVALID_TRANSACTION_TIMEOUT"),
2377            (51, "CONCURRENT_TRANSACTIONS"),
2378            (52, "TRANSACTION_COORDINATOR_FENCED"),
2379            (53, "TRANSACTIONAL_ID_AUTHORIZATION_FAILED"),
2380            (54, "SECURITY_DISABLED"),
2381            (55, "OPERATION_NOT_ATTEMPTED"),
2382            (56, "KAFKA_STORAGE_ERROR"),
2383            (57, "LOG_DIR_NOT_FOUND"),
2384            (58, "SASL_AUTHENTICATION_FAILED"),
2385            (59, "UNKNOWN_PRODUCER_ID"),
2386            (60, "REASSIGNMENT_IN_PROGRESS"),
2387            (61, "DELEGATION_TOKEN_AUTH_DISABLED"),
2388            (62, "DELEGATION_TOKEN_NOT_FOUND"),
2389            (63, "DELEGATION_TOKEN_OWNER_MISMATCH"),
2390            (64, "DELEGATION_TOKEN_REQUEST_NOT_ALLOWED"),
2391            (65, "DELEGATION_TOKEN_AUTHORIZATION_FAILED"),
2392            (66, "DELEGATION_TOKEN_EXPIRED"),
2393            (67, "INVALID_PRINCIPAL_TYPE"),
2394            (68, "NON_EMPTY_GROUP"),
2395            (69, "GROUP_ID_NOT_FOUND"),
2396            (70, "FETCH_SESSION_ID_NOT_FOUND"),
2397            (71, "INVALID_FETCH_SESSION_EPOCH"),
2398            (72, "LISTENER_NOT_FOUND"),
2399            (73, "TOPIC_DELETION_DISABLED"),
2400            (74, "FENCED_LEADER_EPOCH"),
2401            (75, "UNKNOWN_LEADER_EPOCH"),
2402            (76, "UNSUPPORTED_COMPRESSION_TYPE"),
2403            (77, "STALE_BROKER_EPOCH"),
2404            (78, "OFFSET_NOT_AVAILABLE"),
2405            (79, "MEMBER_ID_REQUIRED"),
2406            (80, "PREFERRED_LEADER_NOT_AVAILABLE"),
2407            (81, "GROUP_MAX_SIZE_REACHED"),
2408            (82, "FENCED_INSTANCE_ID"),
2409            (83, "ELIGIBLE_LEADERS_NOT_AVAILABLE"),
2410            (84, "ELECTION_NOT_NEEDED"),
2411            (85, "NO_REASSIGNMENT_IN_PROGRESS"),
2412            (86, "GROUP_SUBSCRIBED_TO_TOPIC"),
2413            (87, "INVALID_RECORD"),
2414            (88, "UNSTABLE_OFFSET_COMMIT"),
2415            (89, "THROTTLING_QUOTA_EXCEEDED"),
2416            (90, "PRODUCER_FENCED"),
2417            (91, "RESOURCE_NOT_FOUND"),
2418            (92, "DUPLICATE_RESOURCE"),
2419            (93, "UNACCEPTABLE_CREDENTIAL"),
2420            (94, "INCONSISTENT_VOTER_SET"),
2421            (95, "INVALID_UPDATE_VERSION"),
2422            (96, "FEATURE_UPDATE_FAILED"),
2423            (97, "PRINCIPAL_DESERIALIZATION_FAILURE"),
2424            (98, "SNAPSHOT_NOT_FOUND"),
2425            (99, "POSITION_OUT_OF_RANGE"),
2426            (100, "UNKNOWN_TOPIC_ID"),
2427            (101, "DUPLICATE_BROKER_REGISTRATION"),
2428            (102, "BROKER_ID_NOT_REGISTERED"),
2429            (103, "INCONSISTENT_TOPIC_ID"),
2430            (104, "INCONSISTENT_CLUSTER_ID"),
2431            (105, "TRANSACTIONAL_ID_NOT_FOUND"),
2432            (106, "FETCH_SESSION_TOPIC_ID_ERROR"),
2433            (107, "INELIGIBLE_REPLICA"),
2434            (108, "NEW_LEADER_ELECTED"),
2435            (109, "OFFSET_MOVED_TO_TIERED_STORAGE"),
2436            (110, "FENCED_MEMBER_EPOCH"),
2437            (111, "UNRELEASED_INSTANCE_ID"),
2438            (112, "UNSUPPORTED_ASSIGNOR"),
2439            (113, "STALE_MEMBER_EPOCH"),
2440            (114, "MISMATCHED_ENDPOINT_TYPE"),
2441            (115, "UNSUPPORTED_ENDPOINT_TYPE"),
2442            (116, "UNKNOWN_CONTROLLER_ID"),
2443            (117, "UNKNOWN_SUBSCRIPTION_ID"),
2444            (118, "TELEMETRY_TOO_LARGE"),
2445            (119, "INVALID_REGISTRATION"),
2446            (120, "TRANSACTION_ABORTABLE"),
2447            (121, "INVALID_RECORD_STATE"),
2448            (122, "SHARE_SESSION_NOT_FOUND"),
2449            (123, "INVALID_SHARE_SESSION_EPOCH"),
2450            (124, "FENCED_STATE_EPOCH"),
2451            (125, "INVALID_VOTER_KEY"),
2452            (126, "DUPLICATE_VOTER"),
2453            (127, "VOTER_NOT_FOUND"),
2454            (128, "INVALID_REGULAR_EXPRESSION"),
2455            (129, "REBOOTSTRAP_REQUIRED"),
2456            (130, "STREAMS_INVALID_TOPOLOGY"),
2457            (131, "STREAMS_INVALID_TOPOLOGY_EPOCH"),
2458            (132, "STREAMS_TOPOLOGY_FENCED"),
2459            (133, "SHARE_SESSION_LIMIT_REACHED"),
2460            (134, "GROUP_DELETION_FAILED"),
2461            (135, "STREAMS_TOPOLOGY_DESCRIPTION_UPDATE_FAILED"),
2462            (136, "CONTROLLER_ID_NOT_REGISTERED"),
2463        ];
2464
2465        let names = cases
2466            .iter()
2467            .map(|(code, _)| (*code, kafka_error_name(*code)))
2468            .collect::<Vec<_>>();
2469        assert2::assert!(names == cases.to_vec());
2470        // Kafka's codes are contiguous from -1, so every code outside the
2471        // table is one Kafka does not define.
2472    }
2473
2474    #[yare::parameterized(
2475        minimum = { i16::MIN },
2476        negative = { -2 },
2477        beyond_table = { 137 },
2478        maximum = { i16::MAX },
2479    )]
2480    fn kafka_error_name_unknown_codes_are_unknown(code: i16) {
2481        assert2::assert!(kafka_error_name(code) == "UNKNOWN", "code {code}");
2482    }
2483
2484    #[yare::parameterized(
2485        success = { 0, "NONE" },
2486        topic_exists = { 36, "TOPIC_ALREADY_EXISTS" },
2487        not_controller = { 41, "NOT_CONTROLLER" },
2488    )]
2489    fn kafka_error_name_known_codes(code: i16, want: &str) {
2490        assert2::assert!(kafka_error_name(code) == want);
2491    }
2492
2493    #[yare::parameterized(
2494        cluster_authorization = { 31, "CLUSTER_AUTHORIZATION_FAILED" },
2495        unsupported_sasl = { 33, "UNSUPPORTED_SASL_MECHANISM" },
2496        unsupported_version = { 35, "UNSUPPORTED_VERSION" },
2497        delegation_token_expired = { 66, "DELEGATION_TOKEN_EXPIRED" },
2498        eligible_leaders_unavailable = { 83, "ELIGIBLE_LEADERS_NOT_AVAILABLE" },
2499        election_unnecessary = { 84, "ELECTION_NOT_NEEDED" },
2500        resource_not_found = { 91, "RESOURCE_NOT_FOUND" },
2501        duplicate_resource = { 92, "DUPLICATE_RESOURCE" },
2502        unacceptable_credential = { 93, "UNACCEPTABLE_CREDENTIAL" },
2503    )]
2504    fn users_kafka_error_name_includes_scram_describe_codes(code: i16, want: &str) {
2505        assert2::assert!(kafka_error_name(code) == want);
2506    }
2507
2508    #[test]
2509    fn users_kafka_error_name_includes_ineligible_replica() {
2510        assert2::assert!(kafka_error_name(107) == "INELIGIBLE_REPLICA");
2511    }
2512
2513    #[test]
2514    fn kafka_error_name_unknown_returns_unknown() {
2515        assert2::assert!(kafka_error_name(9999) == "UNKNOWN");
2516    }
2517
2518    #[test]
2519    fn kafka_error_if_zero_code_is_none() {
2520        assert2::assert!(kafka_error_if(0, None).is_none());
2521    }
2522
2523    #[test]
2524    fn kafka_error_if_nonzero_carries_name() {
2525        let e = kafka_error_if(36, Some("dup".into())).unwrap();
2526        assert2::assert!(
2527            e == KafkaError {
2528                code: 36,
2529                name: "TOPIC_ALREADY_EXISTS",
2530                message: Some("dup".to_owned()),
2531            }
2532        );
2533    }
2534
2535    #[tokio::test]
2536    async fn connect_secured_threads_security_and_fails_to_closed_port() {
2537        use krabka_security::ListenerProtocol;
2538        use security::{ClientSecurity, SaslCredentials};
2539
2540        let security = ClientSecurity {
2541            protocol: ListenerProtocol::SaslPlaintext,
2542            tls: None,
2543            sasl: Some(SaslCredentials::Plain {
2544                username: "u".into(),
2545                password: "p".into(),
2546            }),
2547            sasl_host: None,
2548        };
2549        // 127.0.0.1:1 has no listener; the secured connect must fail —
2550        // proving the security arg is threaded (not a type error).
2551        let res = AdminClient::connect_secured(&["127.0.0.1:1".to_owned()], Some(security)).await;
2552        assert2::assert!(res.is_err());
2553    }
2554
2555    #[tokio::test(start_paused = true)]
2556    async fn dns_lookup_stops_at_connection_option_deadline() {
2557        let timeout = ClientDnsTimeout::new(Time::from_millis(37)).expect("positive timeout");
2558        let started = Instant::now();
2559        let pending = pending::<io::Result<vec::IntoIter<SocketAddr>>>();
2560
2561        let result = lookup_first("broker.invalid:9092", timeout, pending).await;
2562
2563        assert2::assert!(result.is_err());
2564        assert2::assert!(started.elapsed() == Duration::from_millis(37));
2565    }
2566
2567    #[tokio::test]
2568    async fn custom_options_are_observable_on_initial_dial() {
2569        let live = ObservedAdminBroker::start(Duration::ZERO).await;
2570        let mut admin =
2571            AdminClient::connect_with_options(&[live.addr.to_string()], custom_admin_options())
2572                .await
2573                .unwrap();
2574
2575        assert2::assert!(live.observed_custom_security_and_id());
2576        assert_custom_connect_timeout_is_stored(&admin);
2577        metadata_times_out_with_custom_request_timeout(&mut admin).await;
2578
2579        let slow = ObservedAdminBroker::start(Duration::from_millis(300)).await;
2580        let result = timeout(
2581            Duration::from_secs(1),
2582            AdminClient::connect_with_options(&[slow.addr.to_string()], custom_admin_options()),
2583        )
2584        .await
2585        .expect("ApiVersions obeys the stored connect timeout");
2586        assert2::assert!(result.is_err());
2587
2588        live.stop();
2589        slow.stop();
2590    }
2591
2592    /// `connect_with_config` and `connect_controller_with_config` take the
2593    /// settings of `AdminClientConfig` onto the connection and the retry
2594    /// policy of the client, and an invalid setting stops the connect.
2595    #[tokio::test]
2596    async fn connect_with_config_carries_the_settings_and_refuses_an_invalid_one() {
2597        let live = ObservedAdminBroker::start(Duration::ZERO).await;
2598        let config = || AdminClientConfig {
2599            client_id: Some("tool-a".to_owned()),
2600            request_timeout: secs(45),
2601            default_api_timeout: Some(secs(90)),
2602            retries: 4,
2603            retry_backoff: Time::from_millis(30),
2604            retry_backoff_max: Time::from_millis(300),
2605            ..AdminClientConfig::default()
2606        };
2607
2608        let admin = AdminClient::connect_with_config(&[live.addr.to_string()], config())
2609            .await
2610            .expect("admin connects");
2611
2612        assert2::assert!(admin.options.client_id == "tool-a");
2613        assert2::assert!(admin.options.request_timeout == secs(45));
2614        assert2::assert!(
2615            admin.retry
2616                == retry::RetryPolicy {
2617                    timeout: Duration::from_secs(90),
2618                    initial_backoff: Duration::from_millis(30),
2619                    max_backoff: Duration::from_millis(300),
2620                    jitter: admin.retry.jitter,
2621                    max_retries: 4,
2622                }
2623        );
2624        live.stop();
2625
2626        let controller = ObservedController::start(2).await;
2627        let on_controller =
2628            AdminClient::connect_controller_with_config(&[controller.addr.to_string()], config())
2629                .await
2630                .expect("controller admin connects");
2631
2632        assert2::assert!(on_controller.conn.uses_controller_bootstrap());
2633        assert2::assert!(on_controller.options.client_id == "tool-a");
2634        assert2::assert!(on_controller.retry.max_retries == 4);
2635
2636        let refused = AdminClient::connect_with_config(
2637            &["127.0.0.1:1".to_owned()],
2638            AdminClientConfig {
2639                request_timeout: secs(90),
2640                default_api_timeout: Some(secs(60)),
2641                ..AdminClientConfig::default()
2642            },
2643        )
2644        .await;
2645
2646        assert2::assert!(matches!(refused, Err(AdminError::InvalidConfig(_))));
2647    }
2648
2649    #[tokio::test]
2650    async fn connect_with_dns_timeout_preserves_admin_defaults() {
2651        let live = ObservedAdminBroker::start(Duration::ZERO).await;
2652        let timeout = ClientDnsTimeout::new(Time::from_millis(37)).expect("positive timeout");
2653        let admin = AdminClient::connect_with_dns_timeout(&[live.addr.to_string()], timeout)
2654            .await
2655            .expect("admin connects");
2656
2657        assert2::assert!(admin.options.dns_timeout == timeout);
2658        assert2::assert!(admin.options.client_id.starts_with("adminclient-"));
2659        assert2::assert!(admin.options.socket_connection_setup_timeout == secs(10));
2660        assert2::assert!(admin.options.request_timeout == secs(30));
2661        live.stop();
2662    }
2663
2664    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2665    async fn admin_metadata_uses_a_learned_broker_after_the_only_seed_retires() {
2666        let healthy_port = Arc::new(AtomicU16::new(0));
2667        let healthy_calls = Arc::new(AtomicUsize::new(0));
2668        let h_port = Arc::clone(&healthy_port);
2669        let h_calls = Arc::clone(&healthy_calls);
2670        let healthy = MockBroker::start(move |api_key, version, _correlation_id, _body| {
2671            if api_key == api_versions_request::API_KEY {
2672                return Some(admin_mock_api_versions());
2673            }
2674            if api_key == metadata_request::API_KEY {
2675                h_calls.fetch_add(1, Ordering::SeqCst);
2676                return Some(metadata_mock_response(
2677                    version,
2678                    7,
2679                    h_port.load(Ordering::SeqCst),
2680                ));
2681            }
2682            None
2683        })
2684        .await
2685        .unwrap();
2686        healthy_port.store(healthy.addr.port(), Ordering::SeqCst);
2687
2688        let advertised_port = Arc::clone(&healthy_port);
2689        let seed = MockBroker::start(move |api_key, version, _correlation_id, _body| {
2690            if api_key == api_versions_request::API_KEY {
2691                return Some(admin_mock_api_versions());
2692            }
2693            (api_key == metadata_request::API_KEY)
2694                .then(|| metadata_mock_response(version, 7, advertised_port.load(Ordering::SeqCst)))
2695        })
2696        .await
2697        .unwrap();
2698        let admin = AdminClient::connect(&[seed.addr.to_string()])
2699            .await
2700            .expect("admin connects to its seed");
2701        let first = admin
2702            .conn
2703            .send(metadata_request::MetadataRequest::default())
2704            .await
2705            .expect("seed provides last-known broker metadata");
2706        assert2::assert!(first.brokers[0].node_id == 7);
2707
2708        let seed_addr = seed.addr;
2709        seed.stop();
2710        wait_for_admin_mock_closed(seed_addr).await;
2711
2712        let refreshed = admin
2713            .conn
2714            .send(metadata_request::MetadataRequest::default())
2715            .await
2716            .expect("admin refreshes through the learned broker");
2717        assert2::assert!(refreshed.brokers[0].node_id == 7);
2718        assert2::assert!(
2719            healthy_calls.load(Ordering::SeqCst) >= 1,
2720            "recovery must issue metadata through the learned broker"
2721        );
2722    }
2723
2724    #[tokio::test]
2725    async fn recovering_connection_enforces_required_api_version() {
2726        let broker = MockBroker::start(|api_key, _version, _correlation_id, _body| {
2727            (api_key == api_versions_request::API_KEY)
2728                .then(|| admin_mock_api_versions_with_describe_cluster(1))
2729        })
2730        .await
2731        .unwrap();
2732        let admin = AdminClient::connect(&[broker.addr.to_string()])
2733            .await
2734            .expect("admin connects");
2735
2736        let result = admin
2737            .conn
2738            .send_at_least(DescribeClusterRequest::default(), 2)
2739            .await;
2740
2741        assert2::assert!(matches!(
2742            result,
2743            Err(AdminError::Transport(ClientError::IncompatibleVersion {
2744                api_key: describe_cluster_request::API_KEY,
2745                broker_max: 1,
2746                client_min: 2,
2747                ..
2748            }))
2749        ));
2750        broker.stop();
2751    }
2752
2753    #[tokio::test]
2754    async fn controller_bootstrap_discovers_and_connects_active_controller() {
2755        let controller = ObservedController::start(2).await;
2756
2757        let admin = AdminClient::connect_controller(&[controller.addr.to_string()])
2758            .await
2759            .unwrap_or_else(|error| {
2760                panic!(
2761                    "controller bootstrap: {error}; connections={}; endpoint_types={:?}",
2762                    controller.connections.load(Ordering::SeqCst),
2763                    controller.endpoint_types.lock().unwrap(),
2764                )
2765            });
2766
2767        assert2::assert!(admin.conn.uses_controller_bootstrap());
2768        assert2::assert!(controller.connections.load(Ordering::SeqCst) == 2);
2769        assert2::assert!(*controller.endpoint_types.lock().unwrap() == vec![2]);
2770    }
2771
2772    #[tokio::test]
2773    async fn controller_bootstrap_requires_endpoint_type_version() {
2774        let controller = ObservedController::start(0).await;
2775
2776        let result = AdminClient::connect_controller(&[controller.addr.to_string()]).await;
2777
2778        assert2::assert!(matches!(
2779            result,
2780            Err(AdminError::Transport(ClientError::IncompatibleVersion {
2781                api_key: describe_cluster_request::API_KEY,
2782                broker_max: 0,
2783                client_min: 1,
2784                ..
2785            }))
2786        ));
2787        assert2::assert!(controller.endpoint_types.lock().unwrap().is_empty());
2788    }
2789
2790    #[tokio::test]
2791    async fn secured_dns_timeout_preserves_security_and_admin_defaults() {
2792        let live = ObservedAdminBroker::start(Duration::ZERO).await;
2793        let timeout = ClientDnsTimeout::new(Time::from_millis(37)).expect("positive timeout");
2794        let security = ClientSecurity {
2795            protocol: ListenerProtocol::SaslPlaintext,
2796            tls: None,
2797            sasl: Some(SaslCredentials::Plain {
2798                username: "u".into(),
2799                password: "p".into(),
2800            }),
2801            sasl_host: Some("broker.example".into()),
2802        };
2803        let admin = AdminClient::connect_secured_with_dns_timeout(
2804            &[live.addr.to_string()],
2805            Some(security),
2806            timeout,
2807        )
2808        .await
2809        .expect("secured admin connects");
2810
2811        assert2::assert!(admin.options.dns_timeout == timeout);
2812        assert2::assert!(admin.options.security.is_some());
2813        assert2::assert!(admin.options.client_id.starts_with("adminclient-"));
2814        assert2::assert!(admin.options.socket_connection_setup_timeout == secs(10));
2815        assert2::assert!(admin.options.request_timeout == secs(30));
2816        live.stop();
2817    }
2818
2819    #[tokio::test]
2820    async fn custom_options_are_observable_on_controller_reconnect() {
2821        let bootstrap = ObservedAdminBroker::start(Duration::ZERO).await;
2822        let controller = ObservedAdminBroker::start(Duration::ZERO).await;
2823        let slow = ObservedAdminBroker::start(Duration::from_millis(300)).await;
2824        let mut admin = AdminClient::connect_with_options(
2825            &[bootstrap.addr.to_string()],
2826            custom_admin_options(),
2827        )
2828        .await
2829        .unwrap();
2830
2831        admin.reconnect(&controller.addr.to_string()).await.unwrap();
2832        assert2::assert!(controller.observed_custom_security_and_id());
2833        assert_custom_connect_timeout_is_stored(&admin);
2834        metadata_times_out_with_custom_request_timeout(&mut admin).await;
2835        let result = timeout(
2836            Duration::from_secs(1),
2837            admin.reconnect(&slow.addr.to_string()),
2838        )
2839        .await
2840        .expect("reconnected ApiVersions obeys the stored request timeout");
2841        assert2::assert!(result.is_err());
2842
2843        bootstrap.stop();
2844        controller.stop();
2845        slow.stop();
2846    }
2847
2848    #[tokio::test]
2849    async fn custom_options_are_observable_on_bootstrap_reconnect() {
2850        let slow = ObservedAdminBroker::start(Duration::from_millis(300)).await;
2851        let live = ObservedAdminBroker::start(Duration::ZERO).await;
2852        let bootstrap = [slow.addr.to_string(), live.addr.to_string()];
2853        let mut admin = timeout(
2854            Duration::from_secs(1),
2855            AdminClient::connect_with_options(&bootstrap, custom_admin_options()),
2856        )
2857        .await
2858        .expect("custom initial timeout advances to next bootstrap")
2859        .unwrap();
2860
2861        timeout(Duration::from_secs(1), admin.reconnect_bootstrap())
2862            .await
2863            .expect("custom reconnect timeout advances to next bootstrap")
2864            .unwrap();
2865        assert2::assert!(live.connections.load(Ordering::SeqCst) == 2);
2866        assert2::assert!(live.observed_custom_security_and_id());
2867        assert_custom_connect_timeout_is_stored(&admin);
2868        metadata_times_out_with_custom_request_timeout(&mut admin).await;
2869
2870        slow.stop();
2871        live.stop();
2872    }
2873
2874    #[test]
2875    fn existing_connectors_keep_admin_defaults() {
2876        let options = AdminClient::opts(None).unwrap();
2877
2878        assert2::assert!(options.client_id.starts_with("adminclient-"));
2879        assert2::assert!(options.socket_connection_setup_timeout == secs(10));
2880        assert2::assert!(options.connections_max_idle == krabka_units::minutes(5));
2881        assert2::assert!(options.request_timeout == secs(30));
2882        assert2::assert!(options.security.is_none());
2883    }
2884
2885    #[test]
2886    fn evacuation_methods_are_available_on_trait_objects() {
2887        fn compile_calls(client: &mut dyn AdminClientLike) {
2888            drop(client.describe_partition_assignments(&["orders"]));
2889            drop(client.list_partition_reassignments(&BTreeMap::new(), secs(30)));
2890            drop(client.alter_partition_assignments(&BTreeMap::new(), secs(30)));
2891            drop(client.unregister_broker(7));
2892        }
2893
2894        hint::black_box(compile_calls as fn(&mut dyn AdminClientLike));
2895    }
2896}