use std::collections::BTreeMap;
use std::fmt;
use std::time::Duration;
use kafrust_protocol::api::alter_client_quotas::{
AlterClientQuotasEntityV0, AlterClientQuotasEntryV0, AlterClientQuotasOperationV0,
};
use kafrust_protocol::api::alter_partition_reassignments::{
AlterPartitionReassignmentsPartitionV0, AlterPartitionReassignmentsTopicV0,
};
use kafrust_protocol::api::alter_user_scram_credentials::{
AlterUserScramCredentialsDeletionV0, AlterUserScramCredentialsUpsertionV0,
};
use kafrust_protocol::api::create_acls::CreateAclsCreationV1;
use kafrust_protocol::api::create_partitions::{
CreatePartitionsAssignmentV0, CreatePartitionsTopicResultV0, CreatePartitionsTopicV0,
};
use kafrust_protocol::api::create_topics::{
CreateTopicsAssignmentV2, CreateTopicsConfigV2, CreateTopicsTopicResultV2, CreateTopicsTopicV2,
};
use kafrust_protocol::api::delete_acls::{
DeleteAclsFilterResultV1, DeleteAclsFilterV1, DeleteAclsMatchingAclV1,
};
use kafrust_protocol::api::delete_groups::DeleteGroupResultV1;
use kafrust_protocol::api::delete_topics::DeleteTopicsTopicResultV3;
use kafrust_protocol::api::describe_acls::{DescribeAclsEntryV1, DescribeAclsResponseV1};
use kafrust_protocol::api::describe_client_quotas::{
DescribeClientQuotasComponentV0, DescribeClientQuotasEntityV0, DescribeClientQuotasEntryV0,
DescribeClientQuotasResponseV0, DescribeClientQuotasValueV0,
};
use kafrust_protocol::api::describe_configs::{
DescribeConfigsEntryV1, DescribeConfigsResourceV1, DescribeConfigsResultV1,
DescribeConfigsSynonymV1,
};
use kafrust_protocol::api::describe_groups::{DescribeGroupsGroupV1, DescribeGroupsMemberV1};
use kafrust_protocol::api::describe_user_scram_credentials::{
DescribeUserScramCredentialsResponseV0, ScramCredentialInfoV0,
};
use kafrust_protocol::api::incremental_alter_configs::{
IncrementalAlterConfigsEntryV0, IncrementalAlterConfigsResourceResponseV0,
IncrementalAlterConfigsResourceV0,
};
use kafrust_protocol::api::list_groups::ListedGroupV1;
use kafrust_protocol::api::list_partition_reassignments::ListPartitionReassignmentsTopicV0;
use kafrust_protocol::api::metadata::{BrokerMetadata, TopicMetadata};
use kafrust_protocol::api::offset_delete::{
OffsetDeleteRequestPartitionV0, OffsetDeleteRequestTopicV0, OffsetDeleteResponsePartitionV0,
OffsetDeleteResponseTopicV0,
};
use crate::client::Client;
use crate::config::ClientConfig;
use crate::error::{BrokerErrorKind, Error, Result};
use crate::metrics::ClientMetrics;
use crate::scram::{derive_salted_password, ScramHash};
use rand::RngCore;
#[derive(Debug, Clone)]
pub struct AdminClient {
config: ClientConfig,
}
impl AdminClient {
pub fn new(config: ClientConfig) -> Self {
Self { config }
}
pub fn metrics(&self) -> ClientMetrics {
self.config.metrics_ref()
}
#[tracing::instrument(level = "debug", name = "kafka.admin.describe_cluster", skip_all, err)]
pub async fn describe_cluster(&self) -> Result<ClusterDescription> {
let mut client = self.config.clone().connect().await?;
let metadata = client.metadata(Some(Vec::new())).await?;
Ok(ClusterDescription {
controller_id: metadata.controller_id,
brokers: metadata
.brokers
.into_iter()
.map(BrokerDescription::from_protocol)
.collect(),
})
}
#[tracing::instrument(level = "debug", name = "kafka.admin.list_topics", skip_all, err)]
pub async fn list_topics(&self) -> Result<Vec<TopicListing>> {
let mut client = self.config.clone().connect().await?;
let metadata = client.metadata(None).await?;
for topic in &metadata.topics {
if topic.error_code != 0 {
self.config.record_broker_error();
}
for partition in &topic.partitions {
if partition.error_code != 0 {
self.config.record_broker_error();
}
}
}
Ok(metadata
.topics
.into_iter()
.map(TopicListing::from_protocol)
.collect())
}
#[tracing::instrument(
level = "debug",
name = "kafka.admin.describe_acls",
skip_all,
fields(resource_type = ?filter.resource_type, pattern_type = ?filter.pattern_type),
err
)]
pub async fn describe_acls(&self, filter: &AclFilter) -> Result<DescribeAclsResult> {
let mut client = self.config.clone().connect().await?;
let response = client
.describe_acls_v1(
filter.resource_type.code(),
filter.resource_name.clone(),
filter.pattern_type.code(),
filter.principal.clone(),
filter.host.clone(),
filter.operation.code(),
filter.permission_type.code(),
)
.await?;
if response.error_code != 0 {
self.config.record_broker_error();
}
Ok(DescribeAclsResult::from_protocol(response))
}
#[tracing::instrument(
level = "debug",
name = "kafka.admin.create_acls",
skip_all,
fields(acl_count = bindings.len()),
err
)]
pub async fn create_acls(&self, bindings: &[AclBinding]) -> Result<CreateAclsResult> {
let mut client = self.config.clone().connect().await?;
let response = client
.create_acls_v1(bindings.iter().map(AclBinding::as_protocol).collect())
.await?;
for result in &response.results {
if result.error_code != 0 {
self.config.record_broker_error();
}
}
if response.results.len() != bindings.len() {
return Err(Error::ResponseCountMismatch {
operation: "CreateAcls",
expected: bindings.len(),
actual: response.results.len(),
});
}
Ok(CreateAclsResult::from_protocol(response, bindings))
}
#[tracing::instrument(
level = "debug",
name = "kafka.admin.delete_acls",
skip_all,
fields(filter_count = filters.len()),
err
)]
pub async fn delete_acls(&self, filters: &[AclFilter]) -> Result<DeleteAclsResult> {
let mut client = self.config.clone().connect().await?;
let response = client
.delete_acls_v1(filters.iter().map(AclFilter::as_protocol).collect())
.await?;
for result in &response.filter_results {
if result.error_code != 0 || result.matching_acls.iter().any(|acl| acl.error_code != 0)
{
self.config.record_broker_error();
}
}
if response.filter_results.len() != filters.len() {
return Err(Error::ResponseCountMismatch {
operation: "DeleteAcls",
expected: filters.len(),
actual: response.filter_results.len(),
});
}
Ok(DeleteAclsResult::from_protocol(response, filters))
}
#[tracing::instrument(
level = "debug",
name = "kafka.admin.describe_client_quotas",
skip_all,
fields(strict = filter.strict, component_count = filter.components.len()),
err
)]
pub async fn describe_client_quotas(
&self,
filter: &ClientQuotaFilter,
) -> Result<DescribeClientQuotasResult> {
let mut client = self.config.clone().connect().await?;
let response = client
.describe_client_quotas_v0(
filter
.components
.iter()
.map(ClientQuotaFilterComponent::as_protocol)
.collect(),
filter.strict,
)
.await?;
if response.error_code != 0 {
self.config.record_broker_error();
}
Ok(DescribeClientQuotasResult::from_protocol(response))
}
#[tracing::instrument(
level = "debug",
name = "kafka.admin.alter_client_quotas",
skip_all,
fields(alteration_count = alterations.len(), validate_only),
err
)]
pub async fn alter_client_quotas(
&self,
alterations: &[ClientQuotaAlteration],
validate_only: bool,
) -> Result<AlterClientQuotasResult> {
let mut client = self.config.clone().connect().await?;
let response = client
.alter_client_quotas_v0(
alterations
.iter()
.map(ClientQuotaAlteration::as_protocol)
.collect(),
validate_only,
)
.await?;
for result in &response.entries {
if result.error_code != 0 {
self.config.record_broker_error();
}
}
if response.entries.len() != alterations.len() {
return Err(Error::ResponseCountMismatch {
operation: "AlterClientQuotas",
expected: alterations.len(),
actual: response.entries.len(),
});
}
Ok(AlterClientQuotasResult::from_protocol(
response,
alterations,
))
}
#[tracing::instrument(
level = "debug",
name = "kafka.admin.describe_user_scram_credentials",
skip_all,
fields(user_count = users.map_or(0, <[String]>::len)),
err
)]
pub async fn describe_user_scram_credentials(
&self,
users: Option<&[String]>,
) -> Result<DescribeUserScramCredentialsResult> {
let mut client = self.config.clone().connect().await?;
let response = client
.describe_user_scram_credentials_v0(users.map(ToOwned::to_owned))
.await?;
if response.error_code != 0 {
self.config.record_broker_error();
}
for result in &response.results {
if result.error_code != 0 {
self.config.record_broker_error();
}
}
Ok(DescribeUserScramCredentialsResult::from_protocol(response))
}
#[tracing::instrument(
level = "debug",
name = "kafka.admin.alter_user_scram_credentials",
skip_all,
fields(deletion_count = deletions.len(), upsertion_count = upsertions.len()),
err
)]
pub async fn alter_user_scram_credentials(
&self,
deletions: &[ScramCredentialDeletion],
upsertions: &[ScramCredentialUpsertion],
) -> Result<AlterUserScramCredentialsResult> {
let mut client = self.controller_client().await?;
let response = client
.alter_user_scram_credentials_v0(
deletions
.iter()
.map(ScramCredentialDeletion::as_protocol)
.collect(),
upsertions
.iter()
.map(ScramCredentialUpsertion::as_protocol)
.collect(),
)
.await?;
for result in &response.results {
if result.error_code != 0 {
self.config.record_broker_error();
}
}
Ok(AlterUserScramCredentialsResult::from_protocol(response))
}
#[tracing::instrument(
level = "debug",
name = "kafka.admin.alter_partition_reassignments",
skip_all,
fields(topic_count = reassignments.len()),
err
)]
pub async fn alter_partition_reassignments(
&self,
reassignments: &[PartitionReassignment],
options: PartitionReassignmentOptions,
) -> Result<AlterPartitionReassignmentsResult> {
let mut controller_client = self.controller_client().await?;
let response = controller_client
.alter_partition_reassignments_v0(
duration_millis_i32(options.timeout),
reassignments
.iter()
.map(PartitionReassignment::as_protocol)
.collect(),
)
.await?;
if response.error_code != 0 {
self.config.record_broker_error();
}
for topic in &response.responses {
for partition in &topic.partitions {
if partition.error_code != 0 {
self.config.record_broker_error();
}
}
}
Ok(AlterPartitionReassignmentsResult::from_protocol(response))
}
#[tracing::instrument(
level = "debug",
name = "kafka.admin.list_partition_reassignments",
skip_all,
fields(topic_filter_count = topics.map_or(0, <[PartitionReassignmentQuery]>::len)),
err
)]
pub async fn list_partition_reassignments(
&self,
topics: Option<&[PartitionReassignmentQuery]>,
options: PartitionReassignmentOptions,
) -> Result<ListPartitionReassignmentsResult> {
let mut controller_client = self.controller_client().await?;
let response = controller_client
.list_partition_reassignments_v0(
duration_millis_i32(options.timeout),
topics.map(|topics| {
topics
.iter()
.map(PartitionReassignmentQuery::as_protocol)
.collect()
}),
)
.await?;
if response.error_code != 0 {
self.config.record_broker_error();
}
Ok(ListPartitionReassignmentsResult::from_protocol(response))
}
#[tracing::instrument(
level = "debug",
name = "kafka.admin.describe_topic_configs",
skip_all,
fields(resource_count = resources.len(), include_synonyms = options.include_synonyms),
err
)]
pub async fn describe_topic_configs(
&self,
resources: &[TopicConfigResource],
options: DescribeConfigsOptions,
) -> Result<DescribeConfigsResult> {
let mut client = self.config.clone().connect().await?;
let response = client
.describe_configs_v1(
resources
.iter()
.map(TopicConfigResource::as_protocol)
.collect(),
options.include_synonyms,
)
.await?;
for resource in &response.results {
if resource.error_code != 0 {
self.config.record_broker_error();
}
}
Ok(DescribeConfigsResult {
throttle_time: Duration::from_millis(nonnegative_i32_to_u64(response.throttle_time_ms)),
resources: response
.results
.into_iter()
.map(ConfigResourceResult::from_protocol)
.collect(),
})
}
#[tracing::instrument(
level = "debug",
name = "kafka.admin.incremental_alter_topic_configs",
skip_all,
fields(resource_count = resources.len(), validate_only = options.validate_only),
err
)]
pub async fn incremental_alter_topic_configs(
&self,
resources: &[TopicConfigAlteration],
options: AlterConfigsOptions,
) -> Result<AlterConfigsResult> {
let mut client = self.config.clone().connect().await?;
let response = client
.incremental_alter_configs_v0(
resources
.iter()
.map(TopicConfigAlteration::as_protocol)
.collect(),
options.validate_only,
)
.await?;
for resource in &response.responses {
if resource.error_code != 0 {
self.config.record_broker_error();
}
}
Ok(AlterConfigsResult {
throttle_time: Duration::from_millis(nonnegative_i32_to_u64(response.throttle_time_ms)),
resources: response
.responses
.into_iter()
.map(AlterConfigResourceResult::from_protocol)
.collect(),
})
}
#[tracing::instrument(
level = "debug",
name = "kafka.admin.describe_consumer_groups",
skip_all,
fields(group_count = group_ids.len()),
err
)]
pub async fn describe_consumer_groups(
&self,
group_ids: &[String],
) -> Result<Vec<ConsumerGroupDescription>> {
let mut descriptions = Vec::with_capacity(group_ids.len());
for group_id in group_ids {
let mut coordinator = self.group_coordinator_client(group_id).await?;
let response = coordinator
.describe_groups_v1(vec![group_id.clone()])
.await?;
let throttle_time =
Duration::from_millis(nonnegative_i32_to_u64(response.throttle_time_ms));
let group = response
.groups
.into_iter()
.find(|group| group.group_id == *group_id)
.ok_or_else(|| Error::MissingGroupDescription {
group_id: group_id.clone(),
})?;
if group.error_code != 0 {
self.config.record_broker_error();
}
descriptions.push(ConsumerGroupDescription::from_protocol(
group,
throttle_time,
));
}
Ok(descriptions)
}
#[tracing::instrument(level = "debug", name = "kafka.admin.list_groups", skip_all, err)]
pub async fn list_groups(&self) -> Result<Vec<GroupListing>> {
let mut bootstrap = self.config.clone().connect().await?;
let metadata = bootstrap.metadata(Some(Vec::new())).await?;
let mut groups = BTreeMap::new();
for broker in metadata.brokers {
let mut client = self
.config
.connect_broker(format!("{}:{}", broker.host, broker.port))
.await?;
let response = client.list_groups_v1().await?;
if response.error_code != 0 {
return Err(self.config.broker_error(
response.error_code,
format!("list groups on broker {}", broker.node_id),
));
}
let throttle_time =
Duration::from_millis(nonnegative_i32_to_u64(response.throttle_time_ms));
for group in response.groups {
groups.insert(
group.group_id.clone(),
GroupListing::from_protocol(group, broker.node_id, throttle_time),
);
}
}
Ok(groups.into_values().collect())
}
#[tracing::instrument(
level = "debug",
name = "kafka.admin.delete_consumer_groups",
skip_all,
fields(group_count = group_ids.len()),
err
)]
pub async fn delete_consumer_groups(
&self,
group_ids: &[String],
) -> Result<Vec<DeleteConsumerGroupResult>> {
let mut results = Vec::with_capacity(group_ids.len());
for group_id in group_ids {
let mut coordinator = self.group_coordinator_client(group_id).await?;
let response = coordinator.delete_groups_v1(vec![group_id.clone()]).await?;
let throttle_time =
Duration::from_millis(nonnegative_i32_to_u64(response.throttle_time_ms));
let result = response
.results
.into_iter()
.find(|result| result.group_id == *group_id)
.ok_or_else(|| Error::MissingDeleteGroupResult {
group_id: group_id.clone(),
})?;
if result.error_code != 0 {
self.config.record_broker_error();
}
results.push(DeleteConsumerGroupResult::from_protocol(
result,
throttle_time,
));
}
Ok(results)
}
#[tracing::instrument(
level = "debug",
name = "kafka.admin.delete_consumer_group_offsets",
skip_all,
fields(group_id, topic_count = topics.len()),
err
)]
pub async fn delete_consumer_group_offsets(
&self,
group_id: &str,
topics: &[ConsumerGroupOffsetDelete],
) -> Result<DeleteConsumerGroupOffsetsResult> {
let mut coordinator = self.group_coordinator_client(group_id).await?;
let response = coordinator
.offset_delete_v0(
group_id,
topics
.iter()
.map(ConsumerGroupOffsetDelete::as_protocol)
.collect(),
)
.await?;
if response.error_code != 0 {
self.config.record_broker_error();
}
for topic in &response.topics {
for partition in &topic.partitions {
if partition.error_code != 0 {
self.config.record_broker_error();
}
}
}
Ok(DeleteConsumerGroupOffsetsResult {
group_id: group_id.to_owned(),
error_code: response.error_code,
throttle_time: Duration::from_millis(nonnegative_i32_to_u64(response.throttle_time_ms)),
topics: response
.topics
.into_iter()
.map(DeleteConsumerGroupOffsetsTopicResult::from_protocol)
.collect(),
})
}
async fn group_coordinator_client(&self, group_id: &str) -> Result<Client> {
let mut bootstrap = self.config.clone().connect().await?;
let coordinator = bootstrap.find_group_coordinator(group_id).await?;
if coordinator.error_code != 0 {
self.config.record_broker_error();
return Err(Error::Broker {
code: coordinator.error_code,
context: format!("find coordinator for consumer group {group_id}"),
});
}
self.config
.connect_broker(format!("{}:{}", coordinator.host, coordinator.port))
.await
}
async fn controller_client(&self) -> Result<Client> {
let mut bootstrap = self.config.clone().connect().await?;
let metadata = bootstrap.metadata(Some(Vec::new())).await?;
let controller = metadata
.brokers
.iter()
.find(|broker| broker.node_id == metadata.controller_id)
.ok_or(Error::MissingBroker {
node_id: metadata.controller_id,
})?;
self.config
.connect_broker(format!("{}:{}", controller.host, controller.port))
.await
}
#[tracing::instrument(
level = "debug",
name = "kafka.admin.create_topics",
skip_all,
fields(topic_count = topics.len(), validate_only = options.validate_only),
err
)]
pub async fn create_topics(
&self,
topics: &[NewTopic],
options: CreateTopicsOptions,
) -> Result<CreateTopicsResult> {
let mut controller_client = self.controller_client().await?;
let response = controller_client
.create_topics_v2(
topics.iter().map(NewTopic::as_protocol).collect(),
duration_millis_i32(options.timeout),
options.validate_only,
)
.await?;
for topic in &response.topics {
if topic.error_code != 0 {
self.config.record_broker_error();
}
}
Ok(CreateTopicsResult {
throttle_time: Duration::from_millis(nonnegative_i32_to_u64(response.throttle_time_ms)),
topics: response
.topics
.into_iter()
.map(CreateTopicResult::from_protocol)
.collect(),
})
}
#[tracing::instrument(
level = "debug",
name = "kafka.admin.create_partitions",
skip_all,
fields(topic_count = topics.len(), validate_only = options.validate_only),
err
)]
pub async fn create_partitions(
&self,
topics: &[NewPartitions],
options: CreatePartitionsOptions,
) -> Result<CreatePartitionsResult> {
let mut controller_client = self.controller_client().await?;
let response = controller_client
.create_partitions_v0(
topics.iter().map(NewPartitions::as_protocol).collect(),
duration_millis_i32(options.timeout),
options.validate_only,
)
.await?;
for topic in &response.results {
if topic.error_code != 0 {
self.config.record_broker_error();
}
}
Ok(CreatePartitionsResult {
throttle_time: Duration::from_millis(nonnegative_i32_to_u64(response.throttle_time_ms)),
topics: response
.results
.into_iter()
.map(CreatePartitionsTopicResult::from_protocol)
.collect(),
})
}
#[tracing::instrument(
level = "debug",
name = "kafka.admin.delete_topics",
skip_all,
fields(topic_count = topic_names.len()),
err
)]
pub async fn delete_topics(
&self,
topic_names: &[String],
options: DeleteTopicsOptions,
) -> Result<DeleteTopicsResult> {
let mut controller_client = self.controller_client().await?;
let response = controller_client
.delete_topics_v3(topic_names.to_vec(), duration_millis_i32(options.timeout))
.await?;
for topic in &response.topics {
if topic.error_code != 0 {
self.config.record_broker_error();
}
}
Ok(DeleteTopicsResult {
throttle_time: Duration::from_millis(nonnegative_i32_to_u64(response.throttle_time_ms)),
topics: response
.topics
.into_iter()
.map(DeleteTopicResult::from_protocol)
.collect(),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AclResourceType {
Unknown,
Any,
Topic,
Group,
Cluster,
TransactionalId,
DelegationToken,
User,
Other(i8),
}
impl AclResourceType {
fn code(self) -> i8 {
match self {
Self::Unknown => 0,
Self::Any => 1,
Self::Topic => 2,
Self::Group => 3,
Self::Cluster => 4,
Self::TransactionalId => 5,
Self::DelegationToken => 6,
Self::User => 7,
Self::Other(code) => code,
}
}
fn from_code(code: i8) -> Self {
match code {
0 => Self::Unknown,
1 => Self::Any,
2 => Self::Topic,
3 => Self::Group,
4 => Self::Cluster,
5 => Self::TransactionalId,
6 => Self::DelegationToken,
7 => Self::User,
code => Self::Other(code),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AclPatternType {
Unknown,
Any,
Literal,
Prefixed,
Match,
Other(i8),
}
impl AclPatternType {
fn code(self) -> i8 {
match self {
Self::Unknown => 0,
Self::Any => 1,
Self::Match => 2,
Self::Literal => 3,
Self::Prefixed => 4,
Self::Other(code) => code,
}
}
fn from_code(code: i8) -> Self {
match code {
0 => Self::Unknown,
1 => Self::Any,
2 => Self::Match,
3 => Self::Literal,
4 => Self::Prefixed,
code => Self::Other(code),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AclOperation {
Unknown,
Any,
All,
Read,
Write,
Create,
Delete,
Alter,
Describe,
ClusterAction,
DescribeConfigs,
AlterConfigs,
IdempotentWrite,
Other(i8),
}
impl AclOperation {
fn code(self) -> i8 {
match self {
Self::Unknown => 0,
Self::Any => 1,
Self::All => 2,
Self::Read => 3,
Self::Write => 4,
Self::Create => 5,
Self::Delete => 6,
Self::Alter => 7,
Self::Describe => 8,
Self::ClusterAction => 9,
Self::DescribeConfigs => 10,
Self::AlterConfigs => 11,
Self::IdempotentWrite => 12,
Self::Other(code) => code,
}
}
fn from_code(code: i8) -> Self {
match code {
0 => Self::Unknown,
1 => Self::Any,
2 => Self::All,
3 => Self::Read,
4 => Self::Write,
5 => Self::Create,
6 => Self::Delete,
7 => Self::Alter,
8 => Self::Describe,
9 => Self::ClusterAction,
10 => Self::DescribeConfigs,
11 => Self::AlterConfigs,
12 => Self::IdempotentWrite,
code => Self::Other(code),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AclPermissionType {
Unknown,
Any,
Deny,
Allow,
Other(i8),
}
impl AclPermissionType {
fn code(self) -> i8 {
match self {
Self::Unknown => 0,
Self::Any => 1,
Self::Deny => 2,
Self::Allow => 3,
Self::Other(code) => code,
}
}
fn from_code(code: i8) -> Self {
match code {
0 => Self::Unknown,
1 => Self::Any,
2 => Self::Deny,
3 => Self::Allow,
code => Self::Other(code),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AclBinding {
resource_type: AclResourceType,
resource_name: String,
pattern_type: AclPatternType,
principal: String,
host: String,
operation: AclOperation,
permission_type: AclPermissionType,
}
impl AclBinding {
#[allow(clippy::too_many_arguments)]
pub fn new(
resource_type: AclResourceType,
resource_name: impl Into<String>,
pattern_type: AclPatternType,
principal: impl Into<String>,
host: impl Into<String>,
operation: AclOperation,
permission_type: AclPermissionType,
) -> Self {
Self {
resource_type,
resource_name: resource_name.into(),
pattern_type,
principal: principal.into(),
host: host.into(),
operation,
permission_type,
}
}
pub fn resource_type(&self) -> AclResourceType {
self.resource_type
}
pub fn resource_name(&self) -> &str {
&self.resource_name
}
pub fn pattern_type(&self) -> AclPatternType {
self.pattern_type
}
pub fn principal(&self) -> &str {
&self.principal
}
pub fn host(&self) -> &str {
&self.host
}
pub fn operation(&self) -> AclOperation {
self.operation
}
pub fn permission_type(&self) -> AclPermissionType {
self.permission_type
}
fn as_protocol(&self) -> CreateAclsCreationV1 {
CreateAclsCreationV1 {
resource_type: self.resource_type.code(),
resource_name: self.resource_name.clone(),
resource_pattern_type: self.pattern_type.code(),
principal: self.principal.clone(),
host: self.host.clone(),
operation: self.operation.code(),
permission_type: self.permission_type.code(),
}
}
fn from_protocol(
resource_type: i8,
resource_name: String,
pattern_type: i8,
acl: DescribeAclsEntryV1,
) -> Self {
Self::new(
AclResourceType::from_code(resource_type),
resource_name,
AclPatternType::from_code(pattern_type),
acl.principal,
acl.host,
AclOperation::from_code(acl.operation),
AclPermissionType::from_code(acl.permission_type),
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AclFilter {
resource_type: AclResourceType,
resource_name: Option<String>,
pattern_type: AclPatternType,
principal: Option<String>,
host: Option<String>,
operation: AclOperation,
permission_type: AclPermissionType,
}
impl Default for AclFilter {
fn default() -> Self {
Self {
resource_type: AclResourceType::Any,
resource_name: None,
pattern_type: AclPatternType::Any,
principal: None,
host: None,
operation: AclOperation::Any,
permission_type: AclPermissionType::Any,
}
}
}
impl AclFilter {
pub fn any() -> Self {
Self::default()
}
pub fn resource_type(mut self, resource_type: AclResourceType) -> Self {
self.resource_type = resource_type;
self
}
pub fn resource_name(mut self, resource_name: impl Into<String>) -> Self {
self.resource_name = Some(resource_name.into());
self
}
pub fn pattern_type(mut self, pattern_type: AclPatternType) -> Self {
self.pattern_type = pattern_type;
self
}
pub fn principal(mut self, principal: impl Into<String>) -> Self {
self.principal = Some(principal.into());
self
}
pub fn host(mut self, host: impl Into<String>) -> Self {
self.host = Some(host.into());
self
}
pub fn operation(mut self, operation: AclOperation) -> Self {
self.operation = operation;
self
}
pub fn permission_type(mut self, permission_type: AclPermissionType) -> Self {
self.permission_type = permission_type;
self
}
fn as_protocol(&self) -> DeleteAclsFilterV1 {
DeleteAclsFilterV1 {
resource_type_filter: self.resource_type.code(),
resource_name_filter: self.resource_name.clone(),
pattern_type_filter: self.pattern_type.code(),
principal_filter: self.principal.clone(),
host_filter: self.host.clone(),
operation: self.operation.code(),
permission_type: self.permission_type.code(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DescribeAclsResult {
throttle_time: Duration,
error_code: i16,
error_message: Option<String>,
bindings: Vec<AclBinding>,
}
impl DescribeAclsResult {
pub fn throttle_time(&self) -> Duration {
self.throttle_time
}
pub fn error_code(&self) -> i16 {
self.error_code
}
pub fn error_message(&self) -> Option<&str> {
self.error_message.as_deref()
}
pub fn broker_error_kind(&self) -> Option<BrokerErrorKind> {
(self.error_code != 0).then(|| BrokerErrorKind::from_code(self.error_code))
}
pub fn is_success(&self) -> bool {
self.error_code == 0
}
pub fn bindings(&self) -> &[AclBinding] {
&self.bindings
}
}
impl DescribeAclsResult {
fn from_protocol(response: DescribeAclsResponseV1) -> Self {
let bindings = response
.resources
.into_iter()
.flat_map(|resource| {
resource.acls.into_iter().map(move |acl| {
AclBinding::from_protocol(
resource.resource_type,
resource.resource_name.clone(),
resource.pattern_type,
acl,
)
})
})
.collect();
Self {
throttle_time: Duration::from_millis(nonnegative_i32_to_u64(response.throttle_time_ms)),
error_code: response.error_code,
error_message: response.error_message,
bindings,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateAclsEntryResult {
binding: AclBinding,
error_code: i16,
error_message: Option<String>,
}
impl CreateAclsEntryResult {
pub fn binding(&self) -> &AclBinding {
&self.binding
}
pub fn error_code(&self) -> i16 {
self.error_code
}
pub fn error_message(&self) -> Option<&str> {
self.error_message.as_deref()
}
pub fn is_success(&self) -> bool {
self.error_code == 0
}
pub fn broker_error_kind(&self) -> Option<BrokerErrorKind> {
(self.error_code != 0).then(|| BrokerErrorKind::from_code(self.error_code))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateAclsResult {
throttle_time: Duration,
results: Vec<CreateAclsEntryResult>,
}
impl CreateAclsResult {
pub fn throttle_time(&self) -> Duration {
self.throttle_time
}
pub fn results(&self) -> &[CreateAclsEntryResult] {
&self.results
}
pub fn is_success(&self) -> bool {
self.results.iter().all(CreateAclsEntryResult::is_success)
}
pub fn has_errors(&self) -> bool {
!self.is_success()
}
}
impl CreateAclsResult {
fn from_protocol(
response: kafrust_protocol::api::create_acls::CreateAclsResponseV1,
bindings: &[AclBinding],
) -> Self {
Self {
throttle_time: Duration::from_millis(nonnegative_i32_to_u64(response.throttle_time_ms)),
results: response
.results
.into_iter()
.zip(bindings.iter().cloned())
.map(|(result, binding)| CreateAclsEntryResult {
binding,
error_code: result.error_code,
error_message: result.error_message,
})
.collect(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeleteAclsFilterResult {
filter: AclFilter,
error_code: i16,
error_message: Option<String>,
matching_acls: Vec<DeletedAclResult>,
}
impl DeleteAclsFilterResult {
pub fn filter(&self) -> &AclFilter {
&self.filter
}
pub fn error_code(&self) -> i16 {
self.error_code
}
pub fn error_message(&self) -> Option<&str> {
self.error_message.as_deref()
}
pub fn matching_acls(&self) -> &[DeletedAclResult] {
&self.matching_acls
}
pub fn has_errors(&self) -> bool {
self.error_code != 0 || self.matching_acls.iter().any(|acl| !acl.is_success())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeletedAclResult {
binding: AclBinding,
error_code: i16,
error_message: Option<String>,
}
impl DeletedAclResult {
pub fn binding(&self) -> &AclBinding {
&self.binding
}
pub fn error_code(&self) -> i16 {
self.error_code
}
pub fn error_message(&self) -> Option<&str> {
self.error_message.as_deref()
}
pub fn is_success(&self) -> bool {
self.error_code == 0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeleteAclsResult {
throttle_time: Duration,
filter_results: Vec<DeleteAclsFilterResult>,
}
impl DeleteAclsResult {
pub fn throttle_time(&self) -> Duration {
self.throttle_time
}
pub fn filter_results(&self) -> &[DeleteAclsFilterResult] {
&self.filter_results
}
pub fn is_success(&self) -> bool {
self.filter_results
.iter()
.all(|result| !result.has_errors())
}
pub fn has_errors(&self) -> bool {
!self.is_success()
}
}
impl DeleteAclsResult {
fn from_protocol(
response: kafrust_protocol::api::delete_acls::DeleteAclsResponseV1,
filters: &[AclFilter],
) -> Self {
Self {
throttle_time: Duration::from_millis(nonnegative_i32_to_u64(response.throttle_time_ms)),
filter_results: response
.filter_results
.into_iter()
.zip(filters.iter().cloned())
.map(|(result, filter)| DeleteAclsFilterResult::from_protocol(result, filter))
.collect(),
}
}
}
impl DeleteAclsFilterResult {
fn from_protocol(result: DeleteAclsFilterResultV1, filter: AclFilter) -> Self {
Self {
filter,
error_code: result.error_code,
error_message: result.error_message,
matching_acls: result
.matching_acls
.into_iter()
.map(|acl| {
let DeleteAclsMatchingAclV1 {
error_code,
error_message,
resource_type,
resource_name,
pattern_type,
principal,
host,
operation,
permission_type,
} = acl;
DeletedAclResult {
binding: AclBinding::new(
AclResourceType::from_code(resource_type),
resource_name,
AclPatternType::from_code(pattern_type),
principal,
host,
AclOperation::from_code(operation),
AclPermissionType::from_code(permission_type),
),
error_code,
error_message,
}
})
.collect(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClientQuotaMatchType {
Exact,
Default,
Any,
Other(i8),
}
impl ClientQuotaMatchType {
fn code(self) -> i8 {
match self {
Self::Exact => 0,
Self::Default => 1,
Self::Any => 2,
Self::Other(code) => code,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClientQuotaEntityComponent {
entity_type: String,
entity_name: Option<String>,
}
impl ClientQuotaEntityComponent {
pub fn new(entity_type: impl Into<String>, entity_name: Option<impl Into<String>>) -> Self {
Self {
entity_type: entity_type.into(),
entity_name: entity_name.map(Into::into),
}
}
pub fn entity_type(&self) -> &str {
&self.entity_type
}
pub fn entity_name(&self) -> Option<&str> {
self.entity_name.as_deref()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClientQuotaEntity {
components: Vec<ClientQuotaEntityComponent>,
}
impl ClientQuotaEntity {
pub fn new(components: impl IntoIterator<Item = ClientQuotaEntityComponent>) -> Self {
Self {
components: components.into_iter().collect(),
}
}
pub fn user(name: impl Into<String>) -> Self {
Self::new([ClientQuotaEntityComponent::new("user", Some(name))])
}
pub fn client_id(name: impl Into<String>) -> Self {
Self::new([ClientQuotaEntityComponent::new("client-id", Some(name))])
}
pub fn components(&self) -> &[ClientQuotaEntityComponent] {
&self.components
}
fn as_protocol(&self) -> Vec<AlterClientQuotasEntityV0> {
self.components
.iter()
.map(|component| AlterClientQuotasEntityV0 {
entity_type: component.entity_type.clone(),
entity_name: component.entity_name.clone(),
})
.collect()
}
fn from_protocol(components: Vec<DescribeClientQuotasEntityV0>) -> Self {
Self::new(components.into_iter().map(|component| {
ClientQuotaEntityComponent::new(component.entity_type, component.entity_name)
}))
}
fn from_alter_protocol(components: Vec<AlterClientQuotasEntityV0>) -> Self {
Self::new(components.into_iter().map(|component| {
ClientQuotaEntityComponent::new(component.entity_type, component.entity_name)
}))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClientQuotaFilterComponent {
entity_type: String,
match_type: ClientQuotaMatchType,
match_value: Option<String>,
}
impl ClientQuotaFilterComponent {
pub fn new(
entity_type: impl Into<String>,
match_type: ClientQuotaMatchType,
match_value: Option<impl Into<String>>,
) -> Self {
Self {
entity_type: entity_type.into(),
match_type,
match_value: match_value.map(Into::into),
}
}
pub fn entity_type(&self) -> &str {
&self.entity_type
}
pub fn match_type(&self) -> ClientQuotaMatchType {
self.match_type
}
pub fn match_value(&self) -> Option<&str> {
self.match_value.as_deref()
}
fn as_protocol(&self) -> DescribeClientQuotasComponentV0 {
DescribeClientQuotasComponentV0 {
entity_type: self.entity_type.clone(),
match_type: self.match_type.code(),
match_value: self.match_value.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClientQuotaFilter {
components: Vec<ClientQuotaFilterComponent>,
strict: bool,
}
impl ClientQuotaFilter {
pub fn any() -> Self {
Self {
components: Vec::new(),
strict: false,
}
}
pub fn component(mut self, component: ClientQuotaFilterComponent) -> Self {
self.components.push(component);
self
}
pub fn strict(mut self, strict: bool) -> Self {
self.strict = strict;
self
}
pub fn components(&self) -> &[ClientQuotaFilterComponent] {
&self.components
}
pub fn is_strict(&self) -> bool {
self.strict
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ClientQuotaOperation {
key: String,
value: f64,
remove: bool,
}
impl ClientQuotaOperation {
pub fn new(key: impl Into<String>, value: f64, remove: bool) -> Self {
Self {
key: key.into(),
value,
remove,
}
}
pub fn set(key: impl Into<String>, value: f64) -> Self {
Self::new(key, value, false)
}
pub fn remove(key: impl Into<String>) -> Self {
Self::new(key, 0.0, true)
}
pub fn key(&self) -> &str {
&self.key
}
pub fn value(&self) -> f64 {
self.value
}
pub fn is_remove(&self) -> bool {
self.remove
}
fn as_protocol(&self) -> AlterClientQuotasOperationV0 {
AlterClientQuotasOperationV0 {
key: self.key.clone(),
value: self.value,
remove: self.remove,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ClientQuotaAlteration {
entity: ClientQuotaEntity,
operations: Vec<ClientQuotaOperation>,
}
impl ClientQuotaAlteration {
pub fn new(entity: ClientQuotaEntity) -> Self {
Self {
entity,
operations: Vec::new(),
}
}
pub fn set(mut self, key: impl Into<String>, value: f64) -> Self {
self.operations.push(ClientQuotaOperation::set(key, value));
self
}
pub fn remove(mut self, key: impl Into<String>) -> Self {
self.operations.push(ClientQuotaOperation::remove(key));
self
}
pub fn entity(&self) -> &ClientQuotaEntity {
&self.entity
}
pub fn operations(&self) -> &[ClientQuotaOperation] {
&self.operations
}
fn as_protocol(&self) -> AlterClientQuotasEntryV0 {
AlterClientQuotasEntryV0 {
entities: self.entity.as_protocol(),
operations: self
.operations
.iter()
.map(ClientQuotaOperation::as_protocol)
.collect(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ClientQuotaValue {
key: String,
value: f64,
}
impl ClientQuotaValue {
pub fn key(&self) -> &str {
&self.key
}
pub fn value(&self) -> f64 {
self.value
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ClientQuotaEntry {
entity: ClientQuotaEntity,
values: Vec<ClientQuotaValue>,
}
impl ClientQuotaEntry {
pub fn entity(&self) -> &ClientQuotaEntity {
&self.entity
}
pub fn values(&self) -> &[ClientQuotaValue] {
&self.values
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct DescribeClientQuotasResult {
throttle_time: Duration,
error_code: i16,
error_message: Option<String>,
entries: Vec<ClientQuotaEntry>,
}
impl DescribeClientQuotasResult {
pub fn throttle_time(&self) -> Duration {
self.throttle_time
}
pub fn error_code(&self) -> i16 {
self.error_code
}
pub fn error_message(&self) -> Option<&str> {
self.error_message.as_deref()
}
pub fn broker_error_kind(&self) -> Option<BrokerErrorKind> {
(self.error_code != 0).then(|| BrokerErrorKind::from_code(self.error_code))
}
pub fn is_success(&self) -> bool {
self.error_code == 0
}
pub fn entries(&self) -> &[ClientQuotaEntry] {
&self.entries
}
fn from_protocol(response: DescribeClientQuotasResponseV0) -> Self {
Self {
throttle_time: Duration::from_millis(nonnegative_i32_to_u64(response.throttle_time_ms)),
error_code: response.error_code,
error_message: response.error_message,
entries: response
.entries
.into_iter()
.map(ClientQuotaEntry::from_protocol)
.collect(),
}
}
}
impl ClientQuotaEntry {
fn from_protocol(entry: DescribeClientQuotasEntryV0) -> Self {
Self {
entity: ClientQuotaEntity::from_protocol(entry.entities),
values: entry
.values
.into_iter()
.map(|value: DescribeClientQuotasValueV0| ClientQuotaValue {
key: value.key,
value: value.value,
})
.collect(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct AlterClientQuotaEntryResult {
alteration: ClientQuotaAlteration,
error_code: i16,
error_message: Option<String>,
entity: ClientQuotaEntity,
}
impl AlterClientQuotaEntryResult {
pub fn alteration(&self) -> &ClientQuotaAlteration {
&self.alteration
}
pub fn error_code(&self) -> i16 {
self.error_code
}
pub fn error_message(&self) -> Option<&str> {
self.error_message.as_deref()
}
pub fn entity(&self) -> &ClientQuotaEntity {
&self.entity
}
pub fn is_success(&self) -> bool {
self.error_code == 0
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct AlterClientQuotasResult {
throttle_time: Duration,
entries: Vec<AlterClientQuotaEntryResult>,
}
impl AlterClientQuotasResult {
pub fn throttle_time(&self) -> Duration {
self.throttle_time
}
pub fn entries(&self) -> &[AlterClientQuotaEntryResult] {
&self.entries
}
pub fn is_success(&self) -> bool {
self.entries
.iter()
.all(AlterClientQuotaEntryResult::is_success)
}
pub fn has_errors(&self) -> bool {
!self.is_success()
}
fn from_protocol(
response: kafrust_protocol::api::alter_client_quotas::AlterClientQuotasResponseV0,
alterations: &[ClientQuotaAlteration],
) -> Self {
Self {
throttle_time: Duration::from_millis(nonnegative_i32_to_u64(response.throttle_time_ms)),
entries: response
.entries
.into_iter()
.zip(alterations.iter().cloned())
.map(|(result, alteration)| AlterClientQuotaEntryResult {
entity: ClientQuotaEntity::from_alter_protocol(result.entities),
error_code: result.error_code,
error_message: result.error_message,
alteration,
})
.collect(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScramCredentialMechanism {
Sha256,
Sha512,
Other(i8),
}
impl ScramCredentialMechanism {
fn code(self) -> i8 {
match self {
Self::Sha256 => 1,
Self::Sha512 => 2,
Self::Other(code) => code,
}
}
fn hash(self) -> Option<ScramHash> {
match self {
Self::Sha256 => Some(ScramHash::Sha256),
Self::Sha512 => Some(ScramHash::Sha512),
Self::Other(_) => None,
}
}
pub fn from_code(code: i8) -> Self {
match code {
1 => Self::Sha256,
2 => Self::Sha512,
code => Self::Other(code),
}
}
pub fn code_value(self) -> i8 {
self.code()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScramCredentialDeletion {
username: String,
mechanism: ScramCredentialMechanism,
}
impl ScramCredentialDeletion {
pub fn new(username: impl Into<String>, mechanism: ScramCredentialMechanism) -> Result<Self> {
let username = username.into();
if username.is_empty() {
return Err(Error::InvalidScramCredential {
reason: "username must not be empty",
});
}
Ok(Self {
username,
mechanism,
})
}
pub fn username(&self) -> &str {
&self.username
}
pub fn mechanism(&self) -> ScramCredentialMechanism {
self.mechanism
}
fn as_protocol(&self) -> AlterUserScramCredentialsDeletionV0 {
AlterUserScramCredentialsDeletionV0 {
name: self.username.clone(),
mechanism: self.mechanism.code(),
}
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct ScramCredentialUpsertion {
username: String,
mechanism: ScramCredentialMechanism,
iterations: u32,
salt: Vec<u8>,
salted_password: Vec<u8>,
}
impl fmt::Debug for ScramCredentialUpsertion {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ScramCredentialUpsertion")
.field("username", &self.username)
.field("mechanism", &self.mechanism)
.field("iterations", &self.iterations)
.field("salt_len", &self.salt.len())
.field("salted_password_len", &self.salted_password.len())
.finish()
}
}
impl ScramCredentialUpsertion {
pub fn new(
username: impl Into<String>,
mechanism: ScramCredentialMechanism,
iterations: u32,
password: impl AsRef<[u8]>,
) -> Result<Self> {
let mut salt = vec![0; 32];
rand::thread_rng().fill_bytes(&mut salt);
Self::with_salt(username, mechanism, iterations, password, salt)
}
pub fn with_salt(
username: impl Into<String>,
mechanism: ScramCredentialMechanism,
iterations: u32,
password: impl AsRef<[u8]>,
salt: impl Into<Vec<u8>>,
) -> Result<Self> {
let username = username.into();
if username.is_empty() {
return Err(Error::InvalidScramCredential {
reason: "username must not be empty",
});
}
if iterations == 0 {
return Err(Error::InvalidScramCredential {
reason: "iteration count must be greater than zero",
});
}
let iterations_i32 =
i32::try_from(iterations).map_err(|_| Error::InvalidScramCredential {
reason: "iteration count exceeds Kafka's signed 32-bit field",
})?;
let salt = salt.into();
if salt.is_empty() {
return Err(Error::InvalidScramCredential {
reason: "salt must not be empty",
});
}
let hash = mechanism.hash().ok_or(Error::InvalidScramCredential {
reason: "only SCRAM-SHA-256 and SCRAM-SHA-512 can be derived locally",
})?;
let salted_password = derive_salted_password(hash, password.as_ref(), &salt, iterations);
debug_assert!(iterations_i32 > 0);
Ok(Self {
username,
mechanism,
iterations,
salt,
salted_password,
})
}
pub fn username(&self) -> &str {
&self.username
}
pub fn mechanism(&self) -> ScramCredentialMechanism {
self.mechanism
}
pub fn iterations(&self) -> u32 {
self.iterations
}
fn as_protocol(&self) -> AlterUserScramCredentialsUpsertionV0 {
AlterUserScramCredentialsUpsertionV0 {
name: self.username.clone(),
mechanism: self.mechanism.code(),
iterations: self.iterations as i32,
salt: self.salt.clone(),
salted_password: self.salted_password.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScramCredentialInfo {
mechanism: ScramCredentialMechanism,
iterations: i32,
}
impl ScramCredentialInfo {
pub fn mechanism(&self) -> ScramCredentialMechanism {
self.mechanism
}
pub fn iterations(&self) -> i32 {
self.iterations
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScramUserCredentials {
username: String,
error_code: i16,
error_message: Option<String>,
credentials: Vec<ScramCredentialInfo>,
}
impl ScramUserCredentials {
pub fn username(&self) -> &str {
&self.username
}
pub fn error_code(&self) -> i16 {
self.error_code
}
pub fn error_message(&self) -> Option<&str> {
self.error_message.as_deref()
}
pub fn credentials(&self) -> &[ScramCredentialInfo] {
&self.credentials
}
pub fn is_success(&self) -> bool {
self.error_code == 0
}
pub fn broker_error_kind(&self) -> Option<BrokerErrorKind> {
(self.error_code != 0).then(|| BrokerErrorKind::from_code(self.error_code))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DescribeUserScramCredentialsResult {
throttle_time: Duration,
error_code: i16,
error_message: Option<String>,
users: Vec<ScramUserCredentials>,
}
impl DescribeUserScramCredentialsResult {
pub fn throttle_time(&self) -> Duration {
self.throttle_time
}
pub fn error_code(&self) -> i16 {
self.error_code
}
pub fn error_message(&self) -> Option<&str> {
self.error_message.as_deref()
}
pub fn broker_error_kind(&self) -> Option<BrokerErrorKind> {
(self.error_code != 0).then(|| BrokerErrorKind::from_code(self.error_code))
}
pub fn is_success(&self) -> bool {
self.error_code == 0
}
pub fn users(&self) -> &[ScramUserCredentials] {
&self.users
}
pub fn has_errors(&self) -> bool {
!self.is_success() || self.users.iter().any(|user| !user.is_success())
}
fn from_protocol(response: DescribeUserScramCredentialsResponseV0) -> Self {
Self {
throttle_time: Duration::from_millis(nonnegative_i32_to_u64(response.throttle_time_ms)),
error_code: response.error_code,
error_message: response.error_message,
users: response
.results
.into_iter()
.map(|result| ScramUserCredentials {
username: result.user,
error_code: result.error_code,
error_message: result.error_message,
credentials: result
.credential_infos
.into_iter()
.map(|info: ScramCredentialInfoV0| ScramCredentialInfo {
mechanism: ScramCredentialMechanism::from_code(info.mechanism),
iterations: info.iterations,
})
.collect(),
})
.collect(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AlterScramCredentialResult {
username: String,
error_code: i16,
error_message: Option<String>,
}
impl AlterScramCredentialResult {
pub fn username(&self) -> &str {
&self.username
}
pub fn error_code(&self) -> i16 {
self.error_code
}
pub fn error_message(&self) -> Option<&str> {
self.error_message.as_deref()
}
pub fn is_success(&self) -> bool {
self.error_code == 0
}
pub fn broker_error_kind(&self) -> Option<BrokerErrorKind> {
(self.error_code != 0).then(|| BrokerErrorKind::from_code(self.error_code))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AlterUserScramCredentialsResult {
throttle_time: Duration,
results: Vec<AlterScramCredentialResult>,
}
impl AlterUserScramCredentialsResult {
pub fn throttle_time(&self) -> Duration {
self.throttle_time
}
pub fn results(&self) -> &[AlterScramCredentialResult] {
&self.results
}
pub fn is_success(&self) -> bool {
self.results
.iter()
.all(AlterScramCredentialResult::is_success)
}
pub fn has_errors(&self) -> bool {
!self.is_success()
}
fn from_protocol(
response: kafrust_protocol::api::alter_user_scram_credentials::
AlterUserScramCredentialsResponseV0,
) -> Self {
Self {
throttle_time: Duration::from_millis(nonnegative_i32_to_u64(response.throttle_time_ms)),
results: response
.results
.into_iter()
.map(|result| AlterScramCredentialResult {
username: result.user,
error_code: result.error_code,
error_message: result.error_message,
})
.collect(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PartitionReassignment {
topic: String,
partitions: Vec<PartitionReassignmentPartition>,
}
impl PartitionReassignment {
pub fn new(topic: impl Into<String>) -> Self {
Self {
topic: topic.into(),
partitions: Vec::new(),
}
}
pub fn partition(
mut self,
partition_index: i32,
replicas: impl IntoIterator<Item = i32>,
) -> Self {
self.partitions.push(PartitionReassignmentPartition {
partition_index,
replicas: Some(replicas.into_iter().collect()),
});
self
}
pub fn cancel(mut self, partition_index: i32) -> Self {
self.partitions.push(PartitionReassignmentPartition {
partition_index,
replicas: None,
});
self
}
pub fn topic(&self) -> &str {
&self.topic
}
pub fn partitions(&self) -> &[PartitionReassignmentPartition] {
&self.partitions
}
fn as_protocol(&self) -> AlterPartitionReassignmentsTopicV0 {
AlterPartitionReassignmentsTopicV0 {
name: self.topic.clone(),
partitions: self
.partitions
.iter()
.map(PartitionReassignmentPartition::as_protocol)
.collect(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PartitionReassignmentPartition {
partition_index: i32,
replicas: Option<Vec<i32>>,
}
impl PartitionReassignmentPartition {
pub fn partition_index(&self) -> i32 {
self.partition_index
}
pub fn replicas(&self) -> Option<&[i32]> {
self.replicas.as_deref()
}
fn as_protocol(&self) -> AlterPartitionReassignmentsPartitionV0 {
AlterPartitionReassignmentsPartitionV0 {
partition_index: self.partition_index,
replicas: self.replicas.clone(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PartitionReassignmentOptions {
timeout: Duration,
}
impl PartitionReassignmentOptions {
pub fn new() -> Self {
Self::default()
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn request_timeout(&self) -> Duration {
self.timeout
}
}
impl Default for PartitionReassignmentOptions {
fn default() -> Self {
Self {
timeout: Duration::from_secs(30),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PartitionReassignmentQuery {
topic: String,
partition_indexes: Vec<i32>,
}
impl PartitionReassignmentQuery {
pub fn new(topic: impl Into<String>) -> Self {
Self {
topic: topic.into(),
partition_indexes: Vec::new(),
}
}
pub fn partition(mut self, partition_index: i32) -> Self {
self.partition_indexes.push(partition_index);
self
}
pub fn topic(&self) -> &str {
&self.topic
}
pub fn partition_indexes(&self) -> &[i32] {
&self.partition_indexes
}
fn as_protocol(&self) -> ListPartitionReassignmentsTopicV0 {
ListPartitionReassignmentsTopicV0 {
name: self.topic.clone(),
partition_indexes: self.partition_indexes.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AlterPartitionReassignmentResult {
partition_index: i32,
error_code: i16,
error_message: Option<String>,
}
impl AlterPartitionReassignmentResult {
pub fn partition_index(&self) -> i32 {
self.partition_index
}
pub fn error_code(&self) -> i16 {
self.error_code
}
pub fn error_message(&self) -> Option<&str> {
self.error_message.as_deref()
}
pub fn is_success(&self) -> bool {
self.error_code == 0
}
pub fn broker_error_kind(&self) -> Option<BrokerErrorKind> {
(self.error_code != 0).then(|| BrokerErrorKind::from_code(self.error_code))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AlterPartitionReassignmentTopicResult {
name: String,
partitions: Vec<AlterPartitionReassignmentResult>,
}
impl AlterPartitionReassignmentTopicResult {
pub fn name(&self) -> &str {
&self.name
}
pub fn partitions(&self) -> &[AlterPartitionReassignmentResult] {
&self.partitions
}
pub fn is_success(&self) -> bool {
self.partitions
.iter()
.all(AlterPartitionReassignmentResult::is_success)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AlterPartitionReassignmentsResult {
throttle_time: Duration,
error_code: i16,
error_message: Option<String>,
topics: Vec<AlterPartitionReassignmentTopicResult>,
}
impl AlterPartitionReassignmentsResult {
pub fn throttle_time(&self) -> Duration {
self.throttle_time
}
pub fn error_code(&self) -> i16 {
self.error_code
}
pub fn error_message(&self) -> Option<&str> {
self.error_message.as_deref()
}
pub fn broker_error_kind(&self) -> Option<BrokerErrorKind> {
(self.error_code != 0).then(|| BrokerErrorKind::from_code(self.error_code))
}
pub fn topics(&self) -> &[AlterPartitionReassignmentTopicResult] {
&self.topics
}
pub fn is_success(&self) -> bool {
self.error_code == 0
&& self
.topics
.iter()
.all(AlterPartitionReassignmentTopicResult::is_success)
}
pub fn has_errors(&self) -> bool {
!self.is_success()
}
fn from_protocol(
response: kafrust_protocol::api::alter_partition_reassignments::
AlterPartitionReassignmentsResponseV0,
) -> Self {
Self {
throttle_time: Duration::from_millis(nonnegative_i32_to_u64(response.throttle_time_ms)),
error_code: response.error_code,
error_message: response.error_message,
topics: response
.responses
.into_iter()
.map(|topic| AlterPartitionReassignmentTopicResult {
name: topic.name,
partitions: topic
.partitions
.into_iter()
.map(|partition| AlterPartitionReassignmentResult {
partition_index: partition.partition_index,
error_code: partition.error_code,
error_message: partition.error_message,
})
.collect(),
})
.collect(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OngoingPartitionReassignment {
partition_index: i32,
replicas: Vec<i32>,
adding_replicas: Vec<i32>,
removing_replicas: Vec<i32>,
}
impl OngoingPartitionReassignment {
pub fn partition_index(&self) -> i32 {
self.partition_index
}
pub fn replicas(&self) -> &[i32] {
&self.replicas
}
pub fn adding_replicas(&self) -> &[i32] {
&self.adding_replicas
}
pub fn removing_replicas(&self) -> &[i32] {
&self.removing_replicas
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OngoingPartitionReassignmentTopic {
name: String,
partitions: Vec<OngoingPartitionReassignment>,
}
impl OngoingPartitionReassignmentTopic {
pub fn name(&self) -> &str {
&self.name
}
pub fn partitions(&self) -> &[OngoingPartitionReassignment] {
&self.partitions
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListPartitionReassignmentsResult {
throttle_time: Duration,
error_code: i16,
error_message: Option<String>,
topics: Vec<OngoingPartitionReassignmentTopic>,
}
impl ListPartitionReassignmentsResult {
pub fn throttle_time(&self) -> Duration {
self.throttle_time
}
pub fn error_code(&self) -> i16 {
self.error_code
}
pub fn error_message(&self) -> Option<&str> {
self.error_message.as_deref()
}
pub fn broker_error_kind(&self) -> Option<BrokerErrorKind> {
(self.error_code != 0).then(|| BrokerErrorKind::from_code(self.error_code))
}
pub fn topics(&self) -> &[OngoingPartitionReassignmentTopic] {
&self.topics
}
pub fn is_success(&self) -> bool {
self.error_code == 0
}
fn from_protocol(
response: kafrust_protocol::api::list_partition_reassignments::
ListPartitionReassignmentsResponseV0,
) -> Self {
Self {
throttle_time: Duration::from_millis(nonnegative_i32_to_u64(response.throttle_time_ms)),
error_code: response.error_code,
error_message: response.error_message,
topics: response
.topics
.into_iter()
.map(|topic| OngoingPartitionReassignmentTopic {
name: topic.name,
partitions: topic
.partitions
.into_iter()
.map(|partition| OngoingPartitionReassignment {
partition_index: partition.partition_index,
replicas: partition.replicas,
adding_replicas: partition.adding_replicas,
removing_replicas: partition.removing_replicas,
})
.collect(),
})
.collect(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GroupListing {
group_id: String,
protocol_type: String,
coordinator_id: i32,
throttle_time: Duration,
}
impl GroupListing {
pub fn group_id(&self) -> &str {
&self.group_id
}
pub fn protocol_type(&self) -> &str {
&self.protocol_type
}
pub fn coordinator_id(&self) -> i32 {
self.coordinator_id
}
pub fn throttle_time(&self) -> Duration {
self.throttle_time
}
fn from_protocol(group: ListedGroupV1, coordinator_id: i32, throttle_time: Duration) -> Self {
Self {
group_id: group.group_id,
protocol_type: group.protocol_type,
coordinator_id,
throttle_time,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeleteConsumerGroupResult {
group_id: String,
error_code: i16,
throttle_time: Duration,
}
impl DeleteConsumerGroupResult {
pub fn group_id(&self) -> &str {
&self.group_id
}
pub fn is_success(&self) -> bool {
self.error_code == 0
}
pub fn error_code(&self) -> i16 {
self.error_code
}
pub fn broker_error_kind(&self) -> Option<BrokerErrorKind> {
(self.error_code != 0).then(|| BrokerErrorKind::from_code(self.error_code))
}
pub fn throttle_time(&self) -> Duration {
self.throttle_time
}
fn from_protocol(result: DeleteGroupResultV1, throttle_time: Duration) -> Self {
Self {
group_id: result.group_id,
error_code: result.error_code,
throttle_time,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BrokerDescription {
id: i32,
host: String,
port: i32,
rack: Option<String>,
}
impl BrokerDescription {
pub fn id(&self) -> i32 {
self.id
}
pub fn host(&self) -> &str {
&self.host
}
pub fn port(&self) -> i32 {
self.port
}
pub fn rack(&self) -> Option<&str> {
self.rack.as_deref()
}
fn from_protocol(broker: BrokerMetadata) -> Self {
Self {
id: broker.node_id,
host: broker.host,
port: broker.port,
rack: broker.rack,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClusterDescription {
controller_id: i32,
brokers: Vec<BrokerDescription>,
}
impl ClusterDescription {
pub fn controller_id(&self) -> i32 {
self.controller_id
}
pub fn brokers(&self) -> &[BrokerDescription] {
&self.brokers
}
pub fn controller(&self) -> Option<&BrokerDescription> {
self.brokers
.iter()
.find(|broker| broker.id == self.controller_id)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TopicListing {
name: String,
is_internal: bool,
partition_count: usize,
error_code: i16,
}
impl TopicListing {
pub fn name(&self) -> &str {
&self.name
}
pub fn is_internal(&self) -> bool {
self.is_internal
}
pub fn partition_count(&self) -> usize {
self.partition_count
}
pub fn error_code(&self) -> i16 {
self.error_code
}
pub fn is_success(&self) -> bool {
self.error_code == 0
}
pub fn broker_error_kind(&self) -> Option<BrokerErrorKind> {
(self.error_code != 0).then(|| BrokerErrorKind::from_code(self.error_code))
}
fn from_protocol(topic: TopicMetadata) -> Self {
Self {
name: topic.name,
is_internal: topic.is_internal,
partition_count: topic.partitions.len(),
error_code: topic.error_code,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TopicConfigResource {
name: String,
configuration_keys: Option<Vec<String>>,
}
impl TopicConfigResource {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
configuration_keys: None,
}
}
pub fn with_keys(
name: impl Into<String>,
configuration_keys: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
Self {
name: name.into(),
configuration_keys: Some(configuration_keys.into_iter().map(Into::into).collect()),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn configuration_keys(&self) -> Option<&[String]> {
self.configuration_keys.as_deref()
}
fn as_protocol(&self) -> DescribeConfigsResourceV1 {
DescribeConfigsResourceV1 {
resource_type: 2,
resource_name: self.name.clone(),
configuration_keys: self.configuration_keys.clone(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct DescribeConfigsOptions {
include_synonyms: bool,
}
impl DescribeConfigsOptions {
pub fn new() -> Self {
Self::default()
}
pub fn include_synonyms(mut self, include_synonyms: bool) -> Self {
self.include_synonyms = include_synonyms;
self
}
pub fn includes_synonyms(&self) -> bool {
self.include_synonyms
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DescribeConfigsResult {
throttle_time: Duration,
resources: Vec<ConfigResourceResult>,
}
impl DescribeConfigsResult {
pub fn throttle_time(&self) -> Duration {
self.throttle_time
}
pub fn resources(&self) -> &[ConfigResourceResult] {
&self.resources
}
pub fn into_resources(self) -> Vec<ConfigResourceResult> {
self.resources
}
pub fn has_errors(&self) -> bool {
self.resources.iter().any(|resource| !resource.is_success())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConfigResourceResult {
resource_type: i8,
name: String,
error_code: i16,
error_message: Option<String>,
entries: Vec<ConfigEntry>,
}
impl ConfigResourceResult {
pub fn resource_type(&self) -> i8 {
self.resource_type
}
pub fn name(&self) -> &str {
&self.name
}
pub fn is_success(&self) -> bool {
self.error_code == 0
}
pub fn error_code(&self) -> i16 {
self.error_code
}
pub fn error_message(&self) -> Option<&str> {
self.error_message.as_deref()
}
pub fn broker_error_kind(&self) -> Option<BrokerErrorKind> {
(self.error_code != 0).then(|| BrokerErrorKind::from_code(self.error_code))
}
pub fn entries(&self) -> &[ConfigEntry] {
&self.entries
}
fn from_protocol(result: DescribeConfigsResultV1) -> Self {
Self {
resource_type: result.resource_type,
name: result.resource_name,
error_code: result.error_code,
error_message: result.error_message,
entries: result
.configs
.into_iter()
.map(ConfigEntry::from_protocol)
.collect(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConfigEntry {
name: String,
value: Option<String>,
read_only: bool,
source: ConfigSource,
is_sensitive: bool,
synonyms: Vec<ConfigSynonym>,
}
impl ConfigEntry {
pub fn name(&self) -> &str {
&self.name
}
pub fn value(&self) -> Option<&str> {
self.value.as_deref()
}
pub fn is_read_only(&self) -> bool {
self.read_only
}
pub fn source(&self) -> ConfigSource {
self.source
}
pub fn is_sensitive(&self) -> bool {
self.is_sensitive
}
pub fn synonyms(&self) -> &[ConfigSynonym] {
&self.synonyms
}
fn from_protocol(entry: DescribeConfigsEntryV1) -> Self {
Self {
name: entry.name,
value: entry.value,
read_only: entry.read_only,
source: ConfigSource::from_code(entry.config_source),
is_sensitive: entry.is_sensitive,
synonyms: entry
.synonyms
.into_iter()
.map(ConfigSynonym::from_protocol)
.collect(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConfigSynonym {
name: String,
value: Option<String>,
source: ConfigSource,
}
impl ConfigSynonym {
pub fn name(&self) -> &str {
&self.name
}
pub fn value(&self) -> Option<&str> {
self.value.as_deref()
}
pub fn source(&self) -> ConfigSource {
self.source
}
fn from_protocol(synonym: DescribeConfigsSynonymV1) -> Self {
Self {
name: synonym.name,
value: synonym.value,
source: ConfigSource::from_code(synonym.source),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfigSource {
Unknown,
DynamicTopicConfig,
DynamicBrokerConfig,
DynamicDefaultBrokerConfig,
StaticBrokerConfig,
DefaultConfig,
DynamicBrokerLoggerConfig,
DynamicClientMetricsConfig,
DynamicGroupConfig,
Other(i8),
}
impl ConfigSource {
pub fn from_code(code: i8) -> Self {
match code {
0 => Self::Unknown,
1 => Self::DynamicTopicConfig,
2 => Self::DynamicBrokerConfig,
3 => Self::DynamicDefaultBrokerConfig,
4 => Self::StaticBrokerConfig,
5 => Self::DefaultConfig,
6 => Self::DynamicBrokerLoggerConfig,
7 => Self::DynamicClientMetricsConfig,
8 => Self::DynamicGroupConfig,
other => Self::Other(other),
}
}
pub fn code(self) -> i8 {
match self {
Self::Unknown => 0,
Self::DynamicTopicConfig => 1,
Self::DynamicBrokerConfig => 2,
Self::DynamicDefaultBrokerConfig => 3,
Self::StaticBrokerConfig => 4,
Self::DefaultConfig => 5,
Self::DynamicBrokerLoggerConfig => 6,
Self::DynamicClientMetricsConfig => 7,
Self::DynamicGroupConfig => 8,
Self::Other(code) => code,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TopicConfigAlteration {
name: String,
operations: Vec<ConfigAlterOperation>,
}
impl TopicConfigAlteration {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
operations: Vec::new(),
}
}
pub fn set(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.operations.push(ConfigAlterOperation::set(name, value));
self
}
pub fn delete(mut self, name: impl Into<String>) -> Self {
self.operations.push(ConfigAlterOperation::delete(name));
self
}
pub fn append(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.operations
.push(ConfigAlterOperation::append(name, value));
self
}
pub fn subtract(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.operations
.push(ConfigAlterOperation::subtract(name, value));
self
}
pub fn name(&self) -> &str {
&self.name
}
pub fn operations(&self) -> &[ConfigAlterOperation] {
&self.operations
}
fn as_protocol(&self) -> IncrementalAlterConfigsResourceV0 {
IncrementalAlterConfigsResourceV0 {
resource_type: 2,
resource_name: self.name.clone(),
configs: self
.operations
.iter()
.map(ConfigAlterOperation::as_protocol)
.collect(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConfigAlterOperation {
name: String,
kind: ConfigAlterOperationKind,
value: Option<String>,
}
impl ConfigAlterOperation {
pub fn set(name: impl Into<String>, value: impl Into<String>) -> Self {
Self::with_value(name, ConfigAlterOperationKind::Set, value)
}
pub fn delete(name: impl Into<String>) -> Self {
Self {
name: name.into(),
kind: ConfigAlterOperationKind::Delete,
value: None,
}
}
pub fn append(name: impl Into<String>, value: impl Into<String>) -> Self {
Self::with_value(name, ConfigAlterOperationKind::Append, value)
}
pub fn subtract(name: impl Into<String>, value: impl Into<String>) -> Self {
Self::with_value(name, ConfigAlterOperationKind::Subtract, value)
}
pub fn name(&self) -> &str {
&self.name
}
pub fn kind(&self) -> ConfigAlterOperationKind {
self.kind
}
pub fn value(&self) -> Option<&str> {
self.value.as_deref()
}
fn with_value(
name: impl Into<String>,
kind: ConfigAlterOperationKind,
value: impl Into<String>,
) -> Self {
Self {
name: name.into(),
kind,
value: Some(value.into()),
}
}
fn as_protocol(&self) -> IncrementalAlterConfigsEntryV0 {
IncrementalAlterConfigsEntryV0 {
name: self.name.clone(),
operation: self.kind.code(),
value: self.value.clone(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfigAlterOperationKind {
Set,
Delete,
Append,
Subtract,
}
impl ConfigAlterOperationKind {
pub fn code(self) -> i8 {
match self {
Self::Set => 0,
Self::Delete => 1,
Self::Append => 2,
Self::Subtract => 3,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct AlterConfigsOptions {
validate_only: bool,
}
impl AlterConfigsOptions {
pub fn new() -> Self {
Self::default()
}
pub fn validate_only(mut self, validate_only: bool) -> Self {
self.validate_only = validate_only;
self
}
pub fn is_validate_only(&self) -> bool {
self.validate_only
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AlterConfigsResult {
throttle_time: Duration,
resources: Vec<AlterConfigResourceResult>,
}
impl AlterConfigsResult {
pub fn throttle_time(&self) -> Duration {
self.throttle_time
}
pub fn resources(&self) -> &[AlterConfigResourceResult] {
&self.resources
}
pub fn into_resources(self) -> Vec<AlterConfigResourceResult> {
self.resources
}
pub fn has_errors(&self) -> bool {
self.resources.iter().any(|resource| !resource.is_success())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AlterConfigResourceResult {
resource_type: i8,
name: String,
error_code: i16,
error_message: Option<String>,
}
impl AlterConfigResourceResult {
pub fn resource_type(&self) -> i8 {
self.resource_type
}
pub fn name(&self) -> &str {
&self.name
}
pub fn is_success(&self) -> bool {
self.error_code == 0
}
pub fn error_code(&self) -> i16 {
self.error_code
}
pub fn error_message(&self) -> Option<&str> {
self.error_message.as_deref()
}
pub fn broker_error_kind(&self) -> Option<BrokerErrorKind> {
(self.error_code != 0).then(|| BrokerErrorKind::from_code(self.error_code))
}
fn from_protocol(response: IncrementalAlterConfigsResourceResponseV0) -> Self {
Self {
resource_type: response.resource_type,
name: response.resource_name,
error_code: response.error_code,
error_message: response.error_message,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConsumerGroupDescription {
group_id: String,
state: String,
protocol_type: String,
protocol_name: String,
members: Vec<ConsumerGroupMember>,
error_code: i16,
throttle_time: Duration,
}
impl ConsumerGroupDescription {
pub fn group_id(&self) -> &str {
&self.group_id
}
pub fn state(&self) -> &str {
&self.state
}
pub fn protocol_type(&self) -> &str {
&self.protocol_type
}
pub fn protocol_name(&self) -> &str {
&self.protocol_name
}
pub fn members(&self) -> &[ConsumerGroupMember] {
&self.members
}
pub fn is_success(&self) -> bool {
self.error_code == 0
}
pub fn error_code(&self) -> i16 {
self.error_code
}
pub fn broker_error_kind(&self) -> Option<BrokerErrorKind> {
(self.error_code != 0).then(|| BrokerErrorKind::from_code(self.error_code))
}
pub fn throttle_time(&self) -> Duration {
self.throttle_time
}
fn from_protocol(group: DescribeGroupsGroupV1, throttle_time: Duration) -> Self {
Self {
group_id: group.group_id,
state: group.state,
protocol_type: group.protocol_type,
protocol_name: group.protocol_data,
members: group
.members
.into_iter()
.map(ConsumerGroupMember::from_protocol)
.collect(),
error_code: group.error_code,
throttle_time,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConsumerGroupMember {
member_id: String,
client_id: String,
client_host: String,
member_metadata: Vec<u8>,
member_assignment: Vec<u8>,
}
impl ConsumerGroupMember {
pub fn member_id(&self) -> &str {
&self.member_id
}
pub fn client_id(&self) -> &str {
&self.client_id
}
pub fn client_host(&self) -> &str {
&self.client_host
}
pub fn member_metadata(&self) -> &[u8] {
&self.member_metadata
}
pub fn member_assignment(&self) -> &[u8] {
&self.member_assignment
}
fn from_protocol(member: DescribeGroupsMemberV1) -> Self {
Self {
member_id: member.member_id,
client_id: member.client_id,
client_host: member.client_host,
member_metadata: member.member_metadata,
member_assignment: member.member_assignment,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConsumerGroupOffsetDelete {
topic: String,
partitions: Vec<i32>,
}
impl ConsumerGroupOffsetDelete {
pub fn new(topic: impl Into<String>, partitions: impl IntoIterator<Item = i32>) -> Self {
Self {
topic: topic.into(),
partitions: partitions.into_iter().collect(),
}
}
pub fn topic(&self) -> &str {
&self.topic
}
pub fn partitions(&self) -> &[i32] {
&self.partitions
}
fn as_protocol(&self) -> OffsetDeleteRequestTopicV0 {
OffsetDeleteRequestTopicV0 {
name: self.topic.clone(),
partitions: self
.partitions
.iter()
.map(|partition_index| OffsetDeleteRequestPartitionV0 {
partition_index: *partition_index,
})
.collect(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeleteConsumerGroupOffsetsResult {
group_id: String,
error_code: i16,
throttle_time: Duration,
topics: Vec<DeleteConsumerGroupOffsetsTopicResult>,
}
impl DeleteConsumerGroupOffsetsResult {
pub fn group_id(&self) -> &str {
&self.group_id
}
pub fn is_success(&self) -> bool {
self.error_code == 0
&& self
.topics
.iter()
.all(DeleteConsumerGroupOffsetsTopicResult::is_success)
}
pub fn error_code(&self) -> i16 {
self.error_code
}
pub fn broker_error_kind(&self) -> Option<BrokerErrorKind> {
(self.error_code != 0).then(|| BrokerErrorKind::from_code(self.error_code))
}
pub fn throttle_time(&self) -> Duration {
self.throttle_time
}
pub fn topics(&self) -> &[DeleteConsumerGroupOffsetsTopicResult] {
&self.topics
}
pub fn into_topics(self) -> Vec<DeleteConsumerGroupOffsetsTopicResult> {
self.topics
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeleteConsumerGroupOffsetsTopicResult {
topic: String,
partitions: Vec<DeleteConsumerGroupOffsetsPartitionResult>,
}
impl DeleteConsumerGroupOffsetsTopicResult {
pub fn topic(&self) -> &str {
&self.topic
}
pub fn is_success(&self) -> bool {
self.partitions
.iter()
.all(DeleteConsumerGroupOffsetsPartitionResult::is_success)
}
pub fn partitions(&self) -> &[DeleteConsumerGroupOffsetsPartitionResult] {
&self.partitions
}
fn from_protocol(topic: OffsetDeleteResponseTopicV0) -> Self {
Self {
topic: topic.name,
partitions: topic
.partitions
.into_iter()
.map(DeleteConsumerGroupOffsetsPartitionResult::from_protocol)
.collect(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DeleteConsumerGroupOffsetsPartitionResult {
partition_index: i32,
error_code: i16,
}
impl DeleteConsumerGroupOffsetsPartitionResult {
pub fn partition_index(&self) -> i32 {
self.partition_index
}
pub fn is_success(&self) -> bool {
self.error_code == 0
}
pub fn error_code(&self) -> i16 {
self.error_code
}
pub fn broker_error_kind(&self) -> Option<BrokerErrorKind> {
(self.error_code != 0).then(|| BrokerErrorKind::from_code(self.error_code))
}
fn from_protocol(partition: OffsetDeleteResponsePartitionV0) -> Self {
Self {
partition_index: partition.partition_index,
error_code: partition.error_code,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NewTopic {
name: String,
num_partitions: i32,
replication_factor: i16,
assignments: BTreeMap<i32, Vec<i32>>,
configs: BTreeMap<String, Option<String>>,
}
impl NewTopic {
pub fn new(name: impl Into<String>, num_partitions: i32, replication_factor: i16) -> Self {
Self {
name: name.into(),
num_partitions,
replication_factor,
assignments: BTreeMap::new(),
configs: BTreeMap::new(),
}
}
pub fn with_assignments(
name: impl Into<String>,
assignments: impl IntoIterator<Item = (i32, Vec<i32>)>,
) -> Self {
Self {
name: name.into(),
num_partitions: -1,
replication_factor: -1,
assignments: assignments.into_iter().collect(),
configs: BTreeMap::new(),
}
}
pub fn config(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.configs.insert(name.into(), Some(value.into()));
self
}
pub fn nullable_config(mut self, name: impl Into<String>, value: Option<String>) -> Self {
self.configs.insert(name.into(), value);
self
}
pub fn name(&self) -> &str {
&self.name
}
pub fn num_partitions(&self) -> i32 {
self.num_partitions
}
pub fn replication_factor(&self) -> i16 {
self.replication_factor
}
pub fn assignments(&self) -> &BTreeMap<i32, Vec<i32>> {
&self.assignments
}
pub fn configs(&self) -> &BTreeMap<String, Option<String>> {
&self.configs
}
fn as_protocol(&self) -> CreateTopicsTopicV2 {
CreateTopicsTopicV2 {
name: self.name.clone(),
num_partitions: self.num_partitions,
replication_factor: self.replication_factor,
assignments: self
.assignments
.iter()
.map(|(partition_index, broker_ids)| CreateTopicsAssignmentV2 {
partition_index: *partition_index,
broker_ids: broker_ids.clone(),
})
.collect(),
configs: self
.configs
.iter()
.map(|(name, value)| CreateTopicsConfigV2 {
name: name.clone(),
value: value.clone(),
})
.collect(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CreateTopicsOptions {
timeout: Duration,
validate_only: bool,
}
impl CreateTopicsOptions {
pub fn new() -> Self {
Self {
timeout: Duration::from_secs(30),
validate_only: false,
}
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn validate_only(mut self, validate_only: bool) -> Self {
self.validate_only = validate_only;
self
}
pub fn timeout_ref(&self) -> Duration {
self.timeout
}
pub fn is_validate_only(&self) -> bool {
self.validate_only
}
}
impl Default for CreateTopicsOptions {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateTopicsResult {
throttle_time: Duration,
topics: Vec<CreateTopicResult>,
}
impl CreateTopicsResult {
pub fn throttle_time(&self) -> Duration {
self.throttle_time
}
pub fn topics(&self) -> &[CreateTopicResult] {
&self.topics
}
pub fn into_topics(self) -> Vec<CreateTopicResult> {
self.topics
}
pub fn has_errors(&self) -> bool {
self.topics.iter().any(|topic| !topic.is_success())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateTopicResult {
name: String,
error_code: i16,
error_message: Option<String>,
}
impl CreateTopicResult {
pub fn name(&self) -> &str {
&self.name
}
pub fn is_success(&self) -> bool {
self.error_code == 0
}
pub fn error_code(&self) -> i16 {
self.error_code
}
pub fn error_message(&self) -> Option<&str> {
self.error_message.as_deref()
}
pub fn broker_error_kind(&self) -> Option<BrokerErrorKind> {
(self.error_code != 0).then(|| BrokerErrorKind::from_code(self.error_code))
}
fn from_protocol(result: CreateTopicsTopicResultV2) -> Self {
Self {
name: result.name,
error_code: result.error_code,
error_message: result.error_message,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NewPartitions {
name: String,
count: i32,
assignments: Option<Vec<Vec<i32>>>,
}
impl NewPartitions {
pub fn new(name: impl Into<String>, count: i32) -> Self {
Self {
name: name.into(),
count,
assignments: None,
}
}
pub fn with_assignments(
name: impl Into<String>,
count: i32,
assignments: impl IntoIterator<Item = Vec<i32>>,
) -> Self {
Self {
name: name.into(),
count,
assignments: Some(assignments.into_iter().collect()),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn count(&self) -> i32 {
self.count
}
pub fn assignments(&self) -> Option<&[Vec<i32>]> {
self.assignments.as_deref()
}
fn as_protocol(&self) -> CreatePartitionsTopicV0 {
CreatePartitionsTopicV0 {
name: self.name.clone(),
count: self.count,
assignments: self.assignments.as_ref().map(|assignments| {
assignments
.iter()
.map(|broker_ids| CreatePartitionsAssignmentV0 {
broker_ids: broker_ids.clone(),
})
.collect()
}),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CreatePartitionsOptions {
timeout: Duration,
validate_only: bool,
}
impl CreatePartitionsOptions {
pub fn new() -> Self {
Self {
timeout: Duration::from_secs(30),
validate_only: false,
}
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn validate_only(mut self, validate_only: bool) -> Self {
self.validate_only = validate_only;
self
}
pub fn timeout_ref(&self) -> Duration {
self.timeout
}
pub fn is_validate_only(&self) -> bool {
self.validate_only
}
}
impl Default for CreatePartitionsOptions {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreatePartitionsResult {
throttle_time: Duration,
topics: Vec<CreatePartitionsTopicResult>,
}
impl CreatePartitionsResult {
pub fn throttle_time(&self) -> Duration {
self.throttle_time
}
pub fn topics(&self) -> &[CreatePartitionsTopicResult] {
&self.topics
}
pub fn into_topics(self) -> Vec<CreatePartitionsTopicResult> {
self.topics
}
pub fn has_errors(&self) -> bool {
self.topics.iter().any(|topic| !topic.is_success())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreatePartitionsTopicResult {
name: String,
error_code: i16,
error_message: Option<String>,
}
impl CreatePartitionsTopicResult {
pub fn name(&self) -> &str {
&self.name
}
pub fn is_success(&self) -> bool {
self.error_code == 0
}
pub fn error_code(&self) -> i16 {
self.error_code
}
pub fn error_message(&self) -> Option<&str> {
self.error_message.as_deref()
}
pub fn broker_error_kind(&self) -> Option<BrokerErrorKind> {
(self.error_code != 0).then(|| BrokerErrorKind::from_code(self.error_code))
}
fn from_protocol(result: CreatePartitionsTopicResultV0) -> Self {
Self {
name: result.name,
error_code: result.error_code,
error_message: result.error_message,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DeleteTopicsOptions {
timeout: Duration,
}
impl DeleteTopicsOptions {
pub fn new() -> Self {
Self {
timeout: Duration::from_secs(30),
}
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn timeout_ref(&self) -> Duration {
self.timeout
}
}
impl Default for DeleteTopicsOptions {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeleteTopicsResult {
throttle_time: Duration,
topics: Vec<DeleteTopicResult>,
}
impl DeleteTopicsResult {
pub fn throttle_time(&self) -> Duration {
self.throttle_time
}
pub fn topics(&self) -> &[DeleteTopicResult] {
&self.topics
}
pub fn into_topics(self) -> Vec<DeleteTopicResult> {
self.topics
}
pub fn has_errors(&self) -> bool {
self.topics.iter().any(|topic| !topic.is_success())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeleteTopicResult {
name: String,
error_code: i16,
}
impl DeleteTopicResult {
pub fn name(&self) -> &str {
&self.name
}
pub fn is_success(&self) -> bool {
self.error_code == 0
}
pub fn error_code(&self) -> i16 {
self.error_code
}
pub fn broker_error_kind(&self) -> Option<BrokerErrorKind> {
(self.error_code != 0).then(|| BrokerErrorKind::from_code(self.error_code))
}
fn from_protocol(result: DeleteTopicsTopicResultV3) -> Self {
Self {
name: result.name,
error_code: result.error_code,
}
}
}
fn duration_millis_i32(duration: Duration) -> i32 {
i32::try_from(duration.as_millis()).unwrap_or(i32::MAX)
}
fn nonnegative_i32_to_u64(value: i32) -> u64 {
u64::try_from(value).unwrap_or(0)
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::{
AclFilter, AclOperation, AclPatternType, AclPermissionType, AclResourceType, AdminClient,
AlterConfigsOptions, ClientQuotaAlteration, ClientQuotaEntity, ClientQuotaFilter,
ClientQuotaFilterComponent, ClientQuotaMatchType, ConfigAlterOperationKind, ConfigSource,
ConsumerGroupOffsetDelete, CreatePartitionsOptions, CreateTopicsOptions,
DeleteTopicsOptions, DescribeConfigsOptions, NewPartitions, NewTopic,
PartitionReassignment, PartitionReassignmentOptions, PartitionReassignmentQuery,
ScramCredentialDeletion, ScramCredentialMechanism, ScramCredentialUpsertion,
TopicConfigAlteration, TopicConfigResource,
};
use crate::{BrokerErrorKind, ClientConfig, ClientMetrics};
use kafrust_protocol::codec::Encoder;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
#[test]
fn builds_automatic_and_manual_topic_definitions() {
let automatic = NewTopic::new("orders", 6, 3)
.config("cleanup.policy", "compact")
.nullable_config("retention.ms", None);
assert_eq!(automatic.name(), "orders");
assert_eq!(automatic.num_partitions(), 6);
assert_eq!(automatic.replication_factor(), 3);
assert!(automatic.assignments().is_empty());
assert_eq!(
automatic
.configs()
.get("cleanup.policy")
.and_then(|value| value.as_deref()),
Some("compact")
);
assert_eq!(automatic.configs().get("retention.ms"), Some(&None));
let manual = NewTopic::with_assignments("payments", [(0, vec![1, 2]), (1, vec![2, 1])]);
assert_eq!(manual.num_partitions(), -1);
assert_eq!(manual.replication_factor(), -1);
assert_eq!(manual.assignments().get(&1), Some(&vec![2, 1]));
}
#[test]
fn builds_create_topics_options() {
let options = CreateTopicsOptions::new()
.timeout(Duration::from_secs(5))
.validate_only(true);
assert_eq!(options.timeout_ref(), Duration::from_secs(5));
assert!(options.is_validate_only());
}
#[test]
fn builds_partition_expansion_definitions_and_options() {
let automatic = NewPartitions::new("orders", 6);
assert_eq!(automatic.name(), "orders");
assert_eq!(automatic.count(), 6);
assert_eq!(automatic.assignments(), None);
let manual = NewPartitions::with_assignments("payments", 4, [vec![1, 2], vec![2, 1]]);
assert_eq!(manual.count(), 4);
assert_eq!(manual.assignments(), Some(&[vec![1, 2], vec![2, 1]][..]));
let options = CreatePartitionsOptions::new()
.timeout(Duration::from_secs(5))
.validate_only(true);
assert_eq!(options.timeout_ref(), Duration::from_secs(5));
assert!(options.is_validate_only());
}
#[test]
fn builds_delete_topics_options() {
let options = DeleteTopicsOptions::new().timeout(Duration::from_secs(9));
assert_eq!(options.timeout_ref(), Duration::from_secs(9));
}
#[test]
fn builds_topic_config_queries_and_options() {
let all = TopicConfigResource::new("orders");
assert_eq!(all.name(), "orders");
assert_eq!(all.configuration_keys(), None);
let selected =
TopicConfigResource::with_keys("payments", ["cleanup.policy", "retention.ms"]);
assert_eq!(
selected.configuration_keys(),
Some(&["cleanup.policy".to_owned(), "retention.ms".to_owned()][..])
);
let options = DescribeConfigsOptions::new().include_synonyms(true);
assert!(options.includes_synonyms());
assert_eq!(ConfigSource::DynamicTopicConfig.code(), 1);
assert_eq!(ConfigSource::DynamicDefaultBrokerConfig.code(), 3);
assert_eq!(ConfigSource::DynamicGroupConfig.code(), 8);
assert_eq!(ConfigSource::from_code(99), ConfigSource::Other(99));
}
#[test]
fn builds_incremental_topic_config_alterations() {
let alteration = TopicConfigAlteration::new("orders")
.set("retention.ms", "60000")
.delete("segment.ms")
.append("cleanup.policy", "compact")
.subtract("cleanup.policy", "delete");
assert_eq!(alteration.name(), "orders");
assert_eq!(alteration.operations().len(), 4);
assert_eq!(
alteration.operations()[0].kind(),
ConfigAlterOperationKind::Set
);
assert_eq!(alteration.operations()[0].name(), "retention.ms");
assert_eq!(alteration.operations()[0].value(), Some("60000"));
assert_eq!(
alteration.operations()[1].kind(),
ConfigAlterOperationKind::Delete
);
assert_eq!(alteration.operations()[1].value(), None);
assert_eq!(alteration.operations()[2].kind().code(), 2);
assert_eq!(alteration.operations()[3].kind().code(), 3);
let options = AlterConfigsOptions::new().validate_only(true);
assert!(options.is_validate_only());
}
#[tokio::test]
async fn describes_cluster_with_controller_broker() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut connection, _) = listener.accept().await.unwrap();
let request = read_frame(&mut connection).await;
assert_eq!(&request[0..4], &[0, 3, 0, 1]);
assert_eq!(&request[request.len() - 4..], &[0, 0, 0, 0]);
write_frame(&mut connection, &metadata_response(addr.port())).await;
});
let admin =
AdminClient::new(ClientConfig::new([addr.to_string()]).request_timeout_ms(1_000));
let cluster = admin.describe_cluster().await.unwrap();
assert_eq!(cluster.controller_id(), 1);
assert_eq!(cluster.brokers().len(), 1);
assert_eq!(cluster.brokers()[0].id(), 1);
assert_eq!(cluster.brokers()[0].host(), "127.0.0.1");
assert_eq!(cluster.brokers()[0].port(), i32::from(addr.port()));
assert_eq!(cluster.brokers()[0].rack(), None);
assert_eq!(cluster.controller(), Some(&cluster.brokers()[0]));
server.await.unwrap();
}
#[tokio::test]
async fn lists_topics_and_preserves_metadata_errors() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut connection, _) = listener.accept().await.unwrap();
let request = read_frame(&mut connection).await;
assert_eq!(&request[0..4], &[0, 3, 0, 1]);
assert_eq!(&request[request.len() - 4..], &[0xff, 0xff, 0xff, 0xff]);
write_frame(&mut connection, &topic_metadata_response(addr.port())).await;
});
let metrics = ClientMetrics::new();
let admin = AdminClient::new(
ClientConfig::new([addr.to_string()])
.request_timeout_ms(1_000)
.metrics(metrics.clone()),
);
let topics = admin.list_topics().await.unwrap();
assert_eq!(topics.len(), 2);
assert_eq!(topics[0].name(), "orders");
assert!(!topics[0].is_internal());
assert_eq!(topics[0].partition_count(), 1);
assert!(topics[0].is_success());
assert_eq!(topics[0].broker_error_kind(), None);
assert_eq!(topics[1].name(), "__consumer_offsets");
assert!(topics[1].is_internal());
assert_eq!(topics[1].partition_count(), 0);
assert_eq!(topics[1].error_code(), 3);
assert_eq!(
topics[1].broker_error_kind(),
Some(BrokerErrorKind::UnknownTopicOrPartition)
);
assert_eq!(metrics.snapshot().broker_errors, 1);
server.await.unwrap();
}
#[tokio::test]
async fn describes_acls_and_maps_typed_bindings() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut connection, _) = listener.accept().await.unwrap();
let request = read_frame(&mut connection).await;
assert_eq!(&request[0..4], &[0, 29, 0, 1]);
write_frame(&mut connection, &describe_acls_response()).await;
});
let admin = AdminClient::new(ClientConfig::new([addr.to_string()]));
let result = admin
.describe_acls(
&AclFilter::any()
.resource_type(AclResourceType::Topic)
.resource_name("orders")
.pattern_type(AclPatternType::Literal)
.principal("User:alice")
.host("*")
.operation(AclOperation::Read)
.permission_type(AclPermissionType::Allow),
)
.await
.unwrap();
assert!(result.is_success());
assert_eq!(result.bindings().len(), 1);
let binding = &result.bindings()[0];
assert_eq!(binding.resource_type(), AclResourceType::Topic);
assert_eq!(binding.resource_name(), "orders");
assert_eq!(binding.pattern_type(), AclPatternType::Literal);
assert_eq!(binding.principal(), "User:alice");
assert_eq!(binding.operation(), AclOperation::Read);
assert_eq!(binding.permission_type(), AclPermissionType::Allow);
server.await.unwrap();
}
#[tokio::test]
async fn creates_and_deletes_acls_with_partial_results() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut create_connection, _) = listener.accept().await.unwrap();
let create_request = read_frame(&mut create_connection).await;
assert_eq!(&create_request[0..4], &[0, 30, 0, 1]);
write_frame(&mut create_connection, &create_acls_response()).await;
let (mut delete_connection, _) = listener.accept().await.unwrap();
let delete_request = read_frame(&mut delete_connection).await;
assert_eq!(&delete_request[0..4], &[0, 31, 0, 1]);
write_frame(&mut delete_connection, &delete_acls_response()).await;
});
let admin = AdminClient::new(ClientConfig::new([addr.to_string()]));
let bindings = vec![
super::AclBinding::new(
AclResourceType::Topic,
"orders",
AclPatternType::Literal,
"User:alice",
"*",
AclOperation::Read,
AclPermissionType::Allow,
),
super::AclBinding::new(
AclResourceType::Topic,
"payments",
AclPatternType::Literal,
"User:bob",
"10.0.0.1",
AclOperation::Write,
AclPermissionType::Deny,
),
];
let created = admin.create_acls(&bindings).await.unwrap();
assert!(created.has_errors());
assert_eq!(created.results().len(), 2);
assert!(created.results()[0].is_success());
assert_eq!(created.results()[1].error_code(), 29);
assert_eq!(created.results()[1].binding().resource_name(), "payments");
let deleted = admin
.delete_acls(&[AclFilter::any().resource_type(AclResourceType::Topic)])
.await
.unwrap();
assert!(deleted.is_success());
assert_eq!(deleted.filter_results().len(), 1);
assert_eq!(deleted.filter_results()[0].matching_acls().len(), 1);
assert_eq!(
deleted.filter_results()[0].matching_acls()[0]
.binding()
.principal(),
"User:alice"
);
server.await.unwrap();
}
#[tokio::test]
async fn describes_and_alters_client_quotas_with_typed_results() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut describe_connection, _) = listener.accept().await.unwrap();
let describe_request = read_frame(&mut describe_connection).await;
assert_eq!(&describe_request[0..4], &[0, 48, 0, 0]);
write_frame(&mut describe_connection, &describe_client_quotas_response()).await;
let (mut alter_connection, _) = listener.accept().await.unwrap();
let alter_request = read_frame(&mut alter_connection).await;
assert_eq!(&alter_request[0..4], &[0, 49, 0, 0]);
write_frame(&mut alter_connection, &alter_client_quotas_response()).await;
});
let admin = AdminClient::new(ClientConfig::new([addr.to_string()]));
let filter = ClientQuotaFilter::any().component(ClientQuotaFilterComponent::new(
"user",
ClientQuotaMatchType::Exact,
Some("alice"),
));
let described = admin.describe_client_quotas(&filter).await.unwrap();
assert!(described.is_success());
assert_eq!(described.entries().len(), 1);
assert_eq!(
described.entries()[0].entity().components()[0].entity_type(),
"user"
);
assert_eq!(
described.entries()[0].values()[0].key(),
"producer_byte_rate"
);
assert_eq!(described.entries()[0].values()[0].value(), 1024.5);
let altered = admin
.alter_client_quotas(
&[ClientQuotaAlteration::new(ClientQuotaEntity::user("alice"))
.set("producer_byte_rate", 1024.5)],
false,
)
.await
.unwrap();
assert!(altered.is_success());
assert_eq!(altered.entries().len(), 1);
assert_eq!(
altered.entries()[0].entity().components()[0].entity_name(),
Some("alice")
);
server.await.unwrap();
}
#[tokio::test]
async fn describes_and_alters_scram_credentials_with_controller_routing() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut describe_connection, _) = listener.accept().await.unwrap();
let describe_request = read_frame(&mut describe_connection).await;
assert_eq!(&describe_request[0..4], &[0, 50, 0, 0]);
write_frame(
&mut describe_connection,
&describe_user_scram_credentials_response(),
)
.await;
let (mut bootstrap, _) = listener.accept().await.unwrap();
let metadata_request = read_frame(&mut bootstrap).await;
assert_eq!(&metadata_request[0..4], &[0, 3, 0, 1]);
write_frame(&mut bootstrap, &metadata_response(addr.port())).await;
let (mut controller, _) = listener.accept().await.unwrap();
let alter_request = read_frame(&mut controller).await;
assert_eq!(&alter_request[0..4], &[0, 51, 0, 0]);
write_frame(&mut controller, &alter_user_scram_credentials_response()).await;
});
let admin = AdminClient::new(ClientConfig::new([addr.to_string()]));
let users = ["alice".to_owned()];
let described = admin
.describe_user_scram_credentials(Some(&users))
.await
.unwrap();
assert!(described.is_success());
assert_eq!(described.users().len(), 1);
assert_eq!(described.users()[0].username(), "alice");
assert_eq!(described.users()[0].credentials().len(), 2);
assert_eq!(
described.users()[0].credentials()[0].mechanism(),
ScramCredentialMechanism::Sha256
);
assert_eq!(described.users()[0].credentials()[1].iterations(), 8192);
let upsertion = ScramCredentialUpsertion::with_salt(
"alice",
ScramCredentialMechanism::Sha256,
4096,
b"secret",
[1, 2, 3],
)
.unwrap();
let deletion =
ScramCredentialDeletion::new("alice", ScramCredentialMechanism::Sha512).unwrap();
let debug = format!("{upsertion:?}");
assert!(!debug.contains("secret"));
let altered = admin
.alter_user_scram_credentials(&[deletion], &[upsertion])
.await
.unwrap();
assert!(altered.is_success());
assert_eq!(altered.results().len(), 1);
assert_eq!(altered.results()[0].username(), "alice");
server.await.unwrap();
}
#[tokio::test]
async fn alters_and_lists_partition_reassignments_with_controller_routing() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut alter_bootstrap, _) = listener.accept().await.unwrap();
let alter_metadata_request = read_frame(&mut alter_bootstrap).await;
assert_eq!(&alter_metadata_request[0..4], &[0, 3, 0, 1]);
write_frame(&mut alter_bootstrap, &metadata_response(addr.port())).await;
let (mut alter_controller, _) = listener.accept().await.unwrap();
let alter_request = read_frame(&mut alter_controller).await;
assert_eq!(&alter_request[0..4], &[0, 45, 0, 0]);
write_frame(
&mut alter_controller,
&alter_partition_reassignments_response(),
)
.await;
let (mut list_bootstrap, _) = listener.accept().await.unwrap();
let list_metadata_request = read_frame(&mut list_bootstrap).await;
assert_eq!(&list_metadata_request[0..4], &[0, 3, 0, 1]);
write_frame(&mut list_bootstrap, &metadata_response(addr.port())).await;
let (mut list_controller, _) = listener.accept().await.unwrap();
let list_request = read_frame(&mut list_controller).await;
assert_eq!(&list_request[0..4], &[0, 46, 0, 0]);
write_frame(
&mut list_controller,
&list_partition_reassignments_response(),
)
.await;
});
let admin = AdminClient::new(ClientConfig::new([addr.to_string()]));
let request = [PartitionReassignment::new("orders").partition(0, [3, 1, 2])];
let altered = admin
.alter_partition_reassignments(&request, PartitionReassignmentOptions::new())
.await
.unwrap();
assert!(altered.is_success());
assert_eq!(altered.topics()[0].name(), "orders");
assert_eq!(altered.topics()[0].partitions()[0].partition_index(), 0);
let query = [PartitionReassignmentQuery::new("orders").partition(0)];
let ongoing = admin
.list_partition_reassignments(
Some(&query),
PartitionReassignmentOptions::new().timeout(Duration::from_secs(5)),
)
.await
.unwrap();
assert!(ongoing.is_success());
assert_eq!(ongoing.topics()[0].name(), "orders");
assert_eq!(ongoing.topics()[0].partitions()[0].replicas(), [1, 2, 3]);
assert_eq!(ongoing.topics()[0].partitions()[0].adding_replicas(), [3]);
assert_eq!(ongoing.topics()[0].partitions()[0].removing_replicas(), [1]);
server.await.unwrap();
}
#[tokio::test]
async fn describes_topic_configs_and_preserves_resource_errors() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut connection, _) = listener.accept().await.unwrap();
let request = read_frame(&mut connection).await;
assert_eq!(&request[0..4], &[0, 32, 0, 1]);
assert_eq!(request.last(), Some(&1));
write_frame(&mut connection, &describe_configs_response()).await;
});
let metrics = ClientMetrics::new();
let admin = AdminClient::new(
ClientConfig::new([addr.to_string()])
.request_timeout_ms(1_000)
.metrics(metrics.clone()),
);
let result = admin
.describe_topic_configs(
&[
TopicConfigResource::with_keys("orders", ["cleanup.policy"]),
TopicConfigResource::new("missing"),
],
DescribeConfigsOptions::new().include_synonyms(true),
)
.await
.unwrap();
assert_eq!(result.throttle_time(), Duration::from_millis(9));
assert!(result.has_errors());
assert_eq!(result.resources().len(), 2);
let orders = &result.resources()[0];
assert_eq!(orders.resource_type(), 2);
assert_eq!(orders.name(), "orders");
assert!(orders.is_success());
assert_eq!(orders.error_message(), None);
assert_eq!(orders.entries().len(), 1);
assert_eq!(orders.entries()[0].name(), "cleanup.policy");
assert_eq!(orders.entries()[0].value(), Some("compact"));
assert!(!orders.entries()[0].is_read_only());
assert!(!orders.entries()[0].is_sensitive());
assert_eq!(
orders.entries()[0].source(),
ConfigSource::DynamicTopicConfig
);
assert_eq!(orders.entries()[0].synonyms().len(), 1);
assert_eq!(orders.entries()[0].synonyms()[0].name(), "cleanup.policy");
assert_eq!(orders.entries()[0].synonyms()[0].value(), Some("delete"));
assert_eq!(
orders.entries()[0].synonyms()[0].source(),
ConfigSource::DefaultConfig
);
let missing = &result.resources()[1];
assert_eq!(missing.name(), "missing");
assert_eq!(missing.error_code(), 3);
assert_eq!(missing.error_message(), Some("missing"));
assert_eq!(
missing.broker_error_kind(),
Some(BrokerErrorKind::UnknownTopicOrPartition)
);
assert_eq!(metrics.snapshot().broker_errors, 1);
assert_eq!(result.clone().into_resources().len(), 2);
server.await.unwrap();
}
#[tokio::test]
async fn alters_topic_configs_and_preserves_resource_errors() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut connection, _) = listener.accept().await.unwrap();
let request = read_frame(&mut connection).await;
assert_eq!(&request[0..4], &[0, 44, 0, 0]);
assert_eq!(request.last(), Some(&1));
write_frame(&mut connection, &incremental_alter_configs_response()).await;
});
let metrics = ClientMetrics::new();
let admin = AdminClient::new(
ClientConfig::new([addr.to_string()])
.request_timeout_ms(1_000)
.metrics(metrics.clone()),
);
let result = admin
.incremental_alter_topic_configs(
&[
TopicConfigAlteration::new("orders").set("retention.ms", "60000"),
TopicConfigAlteration::new("payments").delete("retention.ms"),
],
AlterConfigsOptions::new().validate_only(true),
)
.await
.unwrap();
assert_eq!(result.throttle_time(), Duration::from_millis(6));
assert!(result.has_errors());
assert_eq!(result.resources().len(), 2);
assert_eq!(result.resources()[0].resource_type(), 2);
assert_eq!(result.resources()[0].name(), "orders");
assert!(result.resources()[0].is_success());
assert_eq!(result.resources()[0].error_message(), None);
assert_eq!(result.resources()[1].name(), "payments");
assert_eq!(result.resources()[1].error_code(), 40);
assert_eq!(result.resources()[1].error_message(), Some("invalid"));
assert_eq!(
result.resources()[1].broker_error_kind(),
Some(BrokerErrorKind::InvalidConfig)
);
assert_eq!(metrics.snapshot().broker_errors, 1);
assert_eq!(result.clone().into_resources().len(), 2);
server.await.unwrap();
}
#[tokio::test]
async fn routes_describe_group_to_coordinator_and_preserves_member_bytes() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut bootstrap, _) = listener.accept().await.unwrap();
let coordinator_request = read_frame(&mut bootstrap).await;
assert_eq!(&coordinator_request[0..4], &[0, 10, 0, 1]);
assert_eq!(coordinator_request.last(), Some(&0));
write_frame(
&mut bootstrap,
&find_group_coordinator_response(addr.port()),
)
.await;
let (mut coordinator, _) = listener.accept().await.unwrap();
let describe_request = read_frame(&mut coordinator).await;
assert_eq!(&describe_request[0..4], &[0, 15, 0, 1]);
write_frame(&mut coordinator, &describe_groups_response()).await;
});
let admin =
AdminClient::new(ClientConfig::new([addr.to_string()]).request_timeout_ms(1_000));
let descriptions = admin
.describe_consumer_groups(&["orders-group".to_owned()])
.await
.unwrap();
assert_eq!(descriptions.len(), 1);
let description = &descriptions[0];
assert_eq!(description.group_id(), "orders-group");
assert_eq!(description.state(), "Stable");
assert_eq!(description.protocol_type(), "consumer");
assert_eq!(description.protocol_name(), "range");
assert!(description.is_success());
assert_eq!(description.error_code(), 0);
assert_eq!(description.broker_error_kind(), None);
assert_eq!(description.throttle_time(), Duration::from_millis(4));
assert_eq!(description.members().len(), 1);
let member = &description.members()[0];
assert_eq!(member.member_id(), "member-1");
assert_eq!(member.client_id(), "client-1");
assert_eq!(member.client_host(), "/127.0.0.1");
assert_eq!(member.member_metadata(), [1, 2]);
assert_eq!(member.member_assignment(), [3, 4, 5]);
server.await.unwrap();
}
#[tokio::test]
async fn lists_groups_from_cluster_brokers_in_group_id_order() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut bootstrap, _) = listener.accept().await.unwrap();
let metadata_request = read_frame(&mut bootstrap).await;
assert_eq!(&metadata_request[0..4], &[0, 3, 0, 1]);
write_frame(&mut bootstrap, &metadata_response(addr.port())).await;
let (mut broker, _) = listener.accept().await.unwrap();
let list_request = read_frame(&mut broker).await;
assert_eq!(&list_request[0..4], &[0, 16, 0, 1]);
write_frame(&mut broker, &list_groups_response()).await;
});
let admin =
AdminClient::new(ClientConfig::new([addr.to_string()]).request_timeout_ms(1_000));
let groups = admin.list_groups().await.unwrap();
assert_eq!(groups.len(), 2);
assert_eq!(groups[0].group_id(), "connect-cluster");
assert_eq!(groups[0].protocol_type(), "connect");
assert_eq!(groups[1].group_id(), "orders-group");
assert_eq!(groups[1].protocol_type(), "consumer");
assert_eq!(groups[1].coordinator_id(), 1);
assert_eq!(groups[1].throttle_time(), Duration::from_millis(7));
server.await.unwrap();
}
#[tokio::test]
async fn routes_delete_group_to_coordinator_and_preserves_error() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut bootstrap, _) = listener.accept().await.unwrap();
let coordinator_request = read_frame(&mut bootstrap).await;
assert_eq!(&coordinator_request[0..4], &[0, 10, 0, 1]);
write_frame(
&mut bootstrap,
&find_group_coordinator_response(addr.port()),
)
.await;
let (mut coordinator, _) = listener.accept().await.unwrap();
let delete_request = read_frame(&mut coordinator).await;
assert_eq!(&delete_request[0..4], &[0, 42, 0, 1]);
write_frame(&mut coordinator, &delete_groups_response()).await;
});
let metrics = ClientMetrics::new();
let admin = AdminClient::new(
ClientConfig::new([addr.to_string()])
.request_timeout_ms(1_000)
.metrics(metrics.clone()),
);
let results = admin
.delete_consumer_groups(&["orders-group".to_owned()])
.await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].group_id(), "orders-group");
assert!(!results[0].is_success());
assert_eq!(results[0].error_code(), 68);
assert_eq!(
results[0].broker_error_kind(),
Some(BrokerErrorKind::NonEmptyGroup)
);
assert_eq!(results[0].throttle_time(), Duration::from_millis(5));
assert_eq!(metrics.snapshot().broker_errors, 1);
server.await.unwrap();
}
#[tokio::test]
async fn routes_offset_delete_to_coordinator_and_preserves_partition_errors() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut bootstrap, _) = listener.accept().await.unwrap();
let coordinator_request = read_frame(&mut bootstrap).await;
assert_eq!(&coordinator_request[0..4], &[0, 10, 0, 1]);
write_frame(
&mut bootstrap,
&find_group_coordinator_response(addr.port()),
)
.await;
let (mut coordinator, _) = listener.accept().await.unwrap();
let delete_request = read_frame(&mut coordinator).await;
assert_eq!(&delete_request[0..4], &[0, 47, 0, 0]);
assert_eq!(
&delete_request[delete_request.len() - 8..],
&[0, 0, 0, 0, 0, 0, 0, 2]
);
write_frame(&mut coordinator, &offset_delete_response()).await;
});
let metrics = ClientMetrics::new();
let admin = AdminClient::new(
ClientConfig::new([addr.to_string()])
.request_timeout_ms(1_000)
.metrics(metrics.clone()),
);
let result = admin
.delete_consumer_group_offsets(
"orders-group",
&[ConsumerGroupOffsetDelete::new("orders", [0, 2])],
)
.await
.unwrap();
assert_eq!(result.group_id(), "orders-group");
assert_eq!(result.error_code(), 0);
assert_eq!(result.broker_error_kind(), None);
assert_eq!(result.throttle_time(), Duration::from_millis(5));
assert!(!result.is_success());
assert_eq!(result.topics().len(), 1);
assert_eq!(result.topics()[0].topic(), "orders");
assert!(!result.topics()[0].is_success());
assert_eq!(result.topics()[0].partitions().len(), 2);
assert!(result.topics()[0].partitions()[0].is_success());
let rejected = result.topics()[0].partitions()[1];
assert_eq!(rejected.partition_index(), 2);
assert_eq!(rejected.error_code(), 86);
assert_eq!(
rejected.broker_error_kind(),
Some(BrokerErrorKind::GroupSubscribedToTopic)
);
assert_eq!(result.clone().into_topics().len(), 1);
assert_eq!(metrics.snapshot().broker_errors, 1);
server.await.unwrap();
}
#[tokio::test]
async fn routes_create_topics_to_controller_and_preserves_partial_result() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut bootstrap, _) = listener.accept().await.unwrap();
let metadata_request = read_frame(&mut bootstrap).await;
assert_eq!(&metadata_request[0..4], &[0, 3, 0, 1]);
assert_eq!(
&metadata_request[metadata_request.len() - 4..],
&[0, 0, 0, 0]
);
write_frame(&mut bootstrap, &metadata_response(addr.port())).await;
let (mut controller, _) = listener.accept().await.unwrap();
let create_request = read_frame(&mut controller).await;
assert_eq!(&create_request[0..4], &[0, 19, 0, 2]);
write_frame(&mut controller, &create_topics_response()).await;
});
let metrics = ClientMetrics::new();
let admin = AdminClient::new(
ClientConfig::new([addr.to_string()])
.request_timeout_ms(1_000)
.metrics(metrics.clone()),
);
let result = admin
.create_topics(&[NewTopic::new("orders", 3, 1)], CreateTopicsOptions::new())
.await
.unwrap();
assert_eq!(result.throttle_time(), Duration::from_millis(7));
assert!(result.has_errors());
assert_eq!(result.topics()[0].name(), "orders");
assert_eq!(result.topics()[0].error_code(), 36);
assert_eq!(result.topics()[0].error_message(), Some("exists"));
assert_eq!(
result.topics()[0].broker_error_kind(),
Some(BrokerErrorKind::TopicAlreadyExists)
);
assert_eq!(metrics.snapshot().broker_errors, 1);
server.await.unwrap();
}
#[tokio::test]
async fn routes_create_partitions_to_controller_and_preserves_partial_result() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut bootstrap, _) = listener.accept().await.unwrap();
let metadata_request = read_frame(&mut bootstrap).await;
assert_eq!(&metadata_request[0..4], &[0, 3, 0, 1]);
write_frame(&mut bootstrap, &metadata_response(addr.port())).await;
let (mut controller, _) = listener.accept().await.unwrap();
let create_request = read_frame(&mut controller).await;
assert_eq!(&create_request[0..4], &[0, 37, 0, 0]);
write_frame(&mut controller, &create_partitions_response()).await;
});
let metrics = ClientMetrics::new();
let admin = AdminClient::new(
ClientConfig::new([addr.to_string()])
.request_timeout_ms(1_000)
.metrics(metrics.clone()),
);
let result = admin
.create_partitions(
&[NewPartitions::new("orders", 3)],
CreatePartitionsOptions::new(),
)
.await
.unwrap();
assert_eq!(result.throttle_time(), Duration::from_millis(6));
assert!(result.has_errors());
assert_eq!(result.topics()[0].name(), "orders");
assert_eq!(result.topics()[0].error_code(), 37);
assert_eq!(result.topics()[0].error_message(), Some("invalid"));
assert_eq!(
result.topics()[0].broker_error_kind(),
Some(BrokerErrorKind::InvalidPartitions)
);
assert_eq!(result.clone().into_topics().len(), 1);
assert_eq!(metrics.snapshot().broker_errors, 1);
server.await.unwrap();
}
#[tokio::test]
async fn routes_delete_topics_to_controller_and_preserves_partial_result() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut bootstrap, _) = listener.accept().await.unwrap();
let metadata_request = read_frame(&mut bootstrap).await;
assert_eq!(&metadata_request[0..4], &[0, 3, 0, 1]);
assert_eq!(
&metadata_request[metadata_request.len() - 4..],
&[0, 0, 0, 0]
);
write_frame(&mut bootstrap, &metadata_response(addr.port())).await;
let (mut controller, _) = listener.accept().await.unwrap();
let delete_request = read_frame(&mut controller).await;
assert_eq!(&delete_request[0..4], &[0, 20, 0, 3]);
write_frame(&mut controller, &delete_topics_response()).await;
});
let metrics = ClientMetrics::new();
let admin = AdminClient::new(
ClientConfig::new([addr.to_string()])
.request_timeout_ms(1_000)
.metrics(metrics.clone()),
);
let result = admin
.delete_topics(&["orders".to_owned()], DeleteTopicsOptions::new())
.await
.unwrap();
assert_eq!(result.throttle_time(), Duration::from_millis(8));
assert!(result.has_errors());
assert_eq!(result.topics()[0].name(), "orders");
assert_eq!(result.topics()[0].error_code(), 3);
assert_eq!(
result.topics()[0].broker_error_kind(),
Some(BrokerErrorKind::UnknownTopicOrPartition)
);
assert_eq!(metrics.snapshot().broker_errors, 1);
server.await.unwrap();
}
async fn read_frame(stream: &mut tokio::net::TcpStream) -> Vec<u8> {
let length = stream.read_i32().await.unwrap();
let mut frame = vec![0; usize::try_from(length).unwrap()];
stream.read_exact(&mut frame).await.unwrap();
frame
}
async fn write_frame(stream: &mut tokio::net::TcpStream, payload: &[u8]) {
stream
.write_i32(i32::try_from(payload.len()).unwrap())
.await
.unwrap();
stream.write_all(payload).await.unwrap();
}
fn metadata_response(port: u16) -> Vec<u8> {
let mut response = vec![
0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 9, b'1', b'2', b'7', b'.', b'0', b'.', b'0', b'.', b'1', ];
response.extend_from_slice(&i32::from(port).to_be_bytes());
response.extend_from_slice(&[
0xff, 0xff, 0, 0, 0, 1, 0, 0, 0, 0, ]);
response
}
fn topic_metadata_response(port: u16) -> Vec<u8> {
let mut response = metadata_response(port);
response.truncate(response.len() - 4);
response.extend_from_slice(&[
0, 0, 0, 2, 0, 0, 0, 6, b'o', b'r', b'd', b'e', b'r', b's', 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 3, 0, 18, b'_', b'_', b'c', b'o', b'n', b's', b'u', b'm', b'e', b'r', b'_', b'o', b'f',
b'f', b's', b'e', b't', b's', 1, 0, 0, 0, 0, ]);
response
}
fn create_topics_response() -> Vec<u8> {
vec![
0, 0, 0, 1, 0, 0, 0, 7, 0, 0, 0, 1, 0, 6, b'o', b'r', b'd', b'e', b'r', b's', 0, 36, 0, 6, b'e', b'x', b'i', b's', b't', b's', ]
}
fn create_partitions_response() -> Vec<u8> {
vec![
0, 0, 0, 1, 0, 0, 0, 6, 0, 0, 0, 1, 0, 6, b'o', b'r', b'd', b'e', b'r', b's', 0, 37, 0, 7, b'i', b'n', b'v', b'a', b'l', b'i', b'd', ]
}
fn delete_topics_response() -> Vec<u8> {
vec![
0, 0, 0, 1, 0, 0, 0, 8, 0, 0, 0, 1, 0, 6, b'o', b'r', b'd', b'e', b'r', b's', 0, 3, ]
}
fn describe_configs_response() -> Vec<u8> {
vec![
0, 0, 0, 1, 0, 0, 0, 9, 0, 0, 0, 2, 0, 0, 0xff, 0xff, 2, 0, 6, b'o', b'r', b'd', b'e', b'r', b's', 0, 0, 0, 1, 0, 14, b'c', b'l', b'e', b'a', b'n', b'u', b'p', b'.', b'p', b'o', b'l', b'i', b'c',
b'y', 0, 7, b'c', b'o', b'm', b'p', b'a', b'c', b't', 0, 1, 0, 0, 0, 0, 1, 0, 14, b'c', b'l', b'e', b'a', b'n', b'u', b'p', b'.', b'p', b'o', b'l', b'i', b'c',
b'y', 0, 6, b'd', b'e', b'l', b'e', b't', b'e', 5, 0, 3, 0, 7, b'm', b'i', b's', b's', b'i', b'n', b'g', 2, 0, 7, b'm', b'i', b's', b's', b'i', b'n', b'g', 0, 0, 0, 0, ]
}
fn describe_acls_response() -> Vec<u8> {
vec![
0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0xff, 0xff, 0, 0, 0, 1, 2, 0, 6, b'o', b'r', b'd', b'e', b'r', b's', 3, 0, 0, 0, 1, 0, 10, b'U', b's', b'e', b'r', b':', b'a', b'l', b'i', b'c', b'e', 0, 1, b'*', 3, 3, ]
}
fn create_acls_response() -> Vec<u8> {
vec![
0, 0, 0, 2, 0, 0, 0, 5, 0, 0, 0, 2, 0, 0, 0xff, 0xff, 0, 29, 0, 7, b'd', b'e', b'n', b'i', b'e', b'd', b'!', ]
}
fn delete_acls_response() -> Vec<u8> {
vec![
0, 0, 0, 3, 0, 0, 0, 6, 0, 0, 0, 1, 0, 0, 0xff, 0xff, 0, 0, 0, 1, 0, 0, 0xff, 0xff, 2, 0, 6, b'o', b'r', b'd', b'e', b'r', b's', 3, 0, 10, b'U', b's', b'e', b'r', b':', b'a', b'l', b'i', b'c', b'e', 0, 1, b'*', 3, 3, ]
}
fn describe_client_quotas_response() -> Vec<u8> {
let mut encoder = Encoder::new();
encoder.write_i32(1); encoder.write_i32(7); encoder.write_i16(0); encoder.write_nullable_string(None).unwrap();
encoder.write_i32(1); encoder.write_i32(1); encoder.write_string("user").unwrap();
encoder.write_nullable_string(Some("alice")).unwrap();
encoder.write_i32(1); encoder.write_string("producer_byte_rate").unwrap();
encoder.write_f64(1024.5);
encoder.into_bytes()
}
fn alter_client_quotas_response() -> Vec<u8> {
let mut encoder = Encoder::new();
encoder.write_i32(2); encoder.write_i32(5); encoder.write_i32(1); encoder.write_i16(0); encoder.write_nullable_string(None).unwrap();
encoder.write_i32(1); encoder.write_string("user").unwrap();
encoder.write_nullable_string(Some("alice")).unwrap();
encoder.into_bytes()
}
fn describe_user_scram_credentials_response() -> Vec<u8> {
let mut encoder = Encoder::new();
encoder.write_i32(1); encoder.write_empty_tagged_fields(); encoder.write_i32(3); encoder.write_i16(0); encoder.write_compact_nullable_string(None).unwrap();
encoder.write_unsigned_varint(2); encoder.write_compact_string("alice").unwrap();
encoder.write_i16(0); encoder.write_compact_nullable_string(None).unwrap();
encoder.write_unsigned_varint(3); encoder.write_i8(1); encoder.write_i32(4096);
encoder.write_empty_tagged_fields();
encoder.write_i8(2); encoder.write_i32(8192);
encoder.write_empty_tagged_fields();
encoder.write_empty_tagged_fields();
encoder.write_empty_tagged_fields();
encoder.into_bytes()
}
fn alter_user_scram_credentials_response() -> Vec<u8> {
let mut encoder = Encoder::new();
encoder.write_i32(1); encoder.write_empty_tagged_fields(); encoder.write_i32(4); encoder.write_unsigned_varint(2); encoder.write_compact_string("alice").unwrap();
encoder.write_i16(0); encoder.write_compact_nullable_string(None).unwrap();
encoder.write_empty_tagged_fields();
encoder.write_empty_tagged_fields();
encoder.into_bytes()
}
fn alter_partition_reassignments_response() -> Vec<u8> {
let mut encoder = Encoder::new();
encoder.write_i32(1); encoder.write_empty_tagged_fields(); encoder.write_i32(4); encoder.write_i16(0); encoder.write_compact_nullable_string(None).unwrap();
encoder.write_unsigned_varint(2); encoder.write_compact_string("orders").unwrap();
encoder.write_unsigned_varint(2); encoder.write_i32(0); encoder.write_i16(0); encoder.write_compact_nullable_string(None).unwrap();
encoder.write_empty_tagged_fields(); encoder.write_empty_tagged_fields(); encoder.write_empty_tagged_fields(); encoder.into_bytes()
}
fn list_partition_reassignments_response() -> Vec<u8> {
let mut encoder = Encoder::new();
encoder.write_i32(1); encoder.write_empty_tagged_fields(); encoder.write_i32(5); encoder.write_i16(0); encoder.write_compact_nullable_string(None).unwrap();
encoder.write_unsigned_varint(2); encoder.write_compact_string("orders").unwrap();
encoder.write_unsigned_varint(2); encoder.write_i32(0); encoder
.write_array(Some(&[1, 2, 3]), |encoder, replica| {
encoder.write_i32(*replica);
Ok(())
})
.unwrap();
encoder
.write_array(Some(&[3]), |encoder, replica| {
encoder.write_i32(*replica);
Ok(())
})
.unwrap();
encoder
.write_array(Some(&[1]), |encoder, replica| {
encoder.write_i32(*replica);
Ok(())
})
.unwrap();
encoder.write_empty_tagged_fields(); encoder.write_empty_tagged_fields(); encoder.write_empty_tagged_fields(); encoder.into_bytes()
}
fn incremental_alter_configs_response() -> Vec<u8> {
vec![
0, 0, 0, 1, 0, 0, 0, 6, 0, 0, 0, 2, 0, 0, 0xff, 0xff, 2, 0, 6, b'o', b'r', b'd', b'e', b'r', b's', 0, 40, 0, 7, b'i', b'n', b'v', b'a', b'l', b'i', b'd', 2, 0, 8, b'p', b'a', b'y', b'm', b'e', b'n', b't', b's', ]
}
fn find_group_coordinator_response(port: u16) -> Vec<u8> {
let mut response = vec![
0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0, 0, 0, 1, 0, 9, b'1', b'2', b'7', b'.', b'0', b'.', b'0', b'.', b'1', ];
response.extend_from_slice(&i32::from(port).to_be_bytes());
response
}
fn describe_groups_response() -> Vec<u8> {
vec![
0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 12, b'o', b'r', b'd', b'e', b'r', b's', b'-', b'g', b'r', b'o', b'u', b'p', 0, 6,
b'S', b't', b'a', b'b', b'l', b'e', 0, 8, b'c', b'o', b'n', b's', b'u', b'm', b'e', b'r', 0, 5, b'r', b'a', b'n', b'g', b'e', 0, 0, 0, 1, 0, 8, b'm', b'e', b'm', b'b', b'e', b'r', b'-', b'1', 0, 8, b'c', b'l', b'i', b'e', b'n', b't', b'-', b'1', 0, 10, b'/', b'1', b'2', b'7', b'.', b'0', b'.', b'0', b'.', b'1', 0, 0, 0, 2, 1, 2, 0, 0, 0, 3, 3, 4, 5, ]
}
fn list_groups_response() -> Vec<u8> {
vec![
0, 0, 0, 1, 0, 0, 0, 7, 0, 0, 0, 0, 0, 2, 0, 12, b'o', b'r', b'd', b'e', b'r', b's', b'-', b'g', b'r', b'o', b'u', b'p', 0, 8,
b'c', b'o', b'n', b's', b'u', b'm', b'e', b'r', 0, 15, b'c', b'o', b'n', b'n', b'e', b'c', b't', b'-', b'c', b'l', b'u', b's', b't',
b'e', b'r', 0, 7, b'c', b'o', b'n', b'n', b'e', b'c', b't', ]
}
fn delete_groups_response() -> Vec<u8> {
vec![
0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0, 1, 0, 12, b'o', b'r', b'd', b'e', b'r', b's', b'-', b'g', b'r', b'o', b'u', b'p', 0,
68,
]
}
fn offset_delete_response() -> Vec<u8> {
vec![
0, 0, 0, 1, 0, 0, 0, 0, 0, 5, 0, 0, 0, 1, 0, 6, b'o', b'r', b'd', b'e', b'r', b's', 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 86, ]
}
}