This is an automated email from the ASF dual-hosted git repository.
hgruszecki pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git
The following commit(s) were added to refs/heads/master by this push:
new 0423f52c9 feat(cli): add cluster metadata command (#2839)
0423f52c9 is described below
commit 0423f52c9d5b700da9119c303c44c0d19e0c1fcd
Author: xin <[email protected]>
AuthorDate: Tue Mar 3 18:20:06 2026 +0900
feat(cli): add cluster metadata command (#2839)
---
.../src/cli/binary_cluster/get_cluster_metadata.rs | 121 ++++++++++++++
.../src/cli/{ => binary_cluster}/mod.rs | 15 +-
core/binary_protocol/src/cli/mod.rs | 1 +
.../src/client/binary_clients/client.rs | 5 +-
.../src/cli/mod.rs => cli/src/args/cluster.rs} | 29 ++--
core/cli/src/args/common.rs | 10 ++
core/cli/src/args/mod.rs | 5 +
core/cli/src/main.rs | 13 +-
.../tests/cli/general/test_help_command.rs | 1 +
.../tests/cli/general/test_overview_command.rs | 1 +
core/integration/tests/cli/system/mod.rs | 1 +
.../cli/system/test_cluster_metadata_command.rs | 184 +++++++++++++++++++++
12 files changed, 353 insertions(+), 33 deletions(-)
diff --git
a/core/binary_protocol/src/cli/binary_cluster/get_cluster_metadata.rs
b/core/binary_protocol/src/cli/binary_cluster/get_cluster_metadata.rs
new file mode 100644
index 000000000..4ed1d95ca
--- /dev/null
+++ b/core/binary_protocol/src/cli/binary_cluster/get_cluster_metadata.rs
@@ -0,0 +1,121 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use crate::Client;
+use crate::cli::cli_command::{CliCommand, PRINT_TARGET};
+use anyhow::Context;
+use async_trait::async_trait;
+use comfy_table::Table;
+use tracing::{Level, event};
+
+pub enum GetClusterMetadataOutput {
+ Table,
+ List,
+}
+
+pub struct GetClusterMetadataCmd {
+ output: GetClusterMetadataOutput,
+}
+
+impl GetClusterMetadataCmd {
+ pub fn new(output: GetClusterMetadataOutput) -> Self {
+ GetClusterMetadataCmd { output }
+ }
+}
+
+impl Default for GetClusterMetadataCmd {
+ fn default() -> Self {
+ GetClusterMetadataCmd {
+ output: GetClusterMetadataOutput::Table,
+ }
+ }
+}
+
+#[async_trait]
+impl CliCommand for GetClusterMetadataCmd {
+ fn explain(&self) -> String {
+ let mode = match self.output {
+ GetClusterMetadataOutput::Table => "table",
+ GetClusterMetadataOutput::List => "list",
+ };
+ format!("get cluster metadata in {mode} mode")
+ }
+
+ async fn execute_cmd(&mut self, client: &dyn Client) -> anyhow::Result<(),
anyhow::Error> {
+ let cluster_metadata = client
+ .get_cluster_metadata()
+ .await
+ .with_context(|| String::from("Problem getting cluster
metadata"))?;
+
+ if cluster_metadata.nodes.is_empty() {
+ event!(target: PRINT_TARGET, Level::INFO, "No cluster nodes
found!");
+ return Ok(());
+ }
+
+ event!(target: PRINT_TARGET, Level::INFO, "Cluster name: {}",
cluster_metadata.name);
+
+ match self.output {
+ GetClusterMetadataOutput::Table => {
+ let mut table = Table::new();
+
+ table.set_header(vec![
+ "Name",
+ "IP",
+ "TCP",
+ "QUIC",
+ "HTTP",
+ "WebSocket",
+ "Role",
+ "Status",
+ ]);
+
+ cluster_metadata.nodes.iter().for_each(|node| {
+ table.add_row(vec![
+ node.name.to_string(),
+ node.ip.to_string(),
+ node.endpoints.tcp.to_string(),
+ node.endpoints.quic.to_string(),
+ node.endpoints.http.to_string(),
+ node.endpoints.websocket.to_string(),
+ node.role.to_string(),
+ node.status.to_string(),
+ ]);
+ });
+
+ event!(target: PRINT_TARGET, Level::INFO, "{table}");
+ }
+ GetClusterMetadataOutput::List => {
+ cluster_metadata.nodes.iter().for_each(|node| {
+ event!(target: PRINT_TARGET, Level::INFO,
+ "{}|{}|{}|{}|{}|{}|{}|{}",
+ node.name,
+ node.ip,
+ node.endpoints.tcp,
+ node.endpoints.quic,
+ node.endpoints.http,
+ node.endpoints.websocket,
+ node.role,
+ node.status
+ );
+ });
+ }
+ }
+
+ Ok(())
+ }
+}
diff --git a/core/binary_protocol/src/cli/mod.rs
b/core/binary_protocol/src/cli/binary_cluster/mod.rs
similarity index 69%
copy from core/binary_protocol/src/cli/mod.rs
copy to core/binary_protocol/src/cli/binary_cluster/mod.rs
index 53f1d9f1b..e8f9e216c 100644
--- a/core/binary_protocol/src/cli/mod.rs
+++ b/core/binary_protocol/src/cli/binary_cluster/mod.rs
@@ -16,17 +16,4 @@
* under the License.
*/
-pub mod binary_client;
-pub mod binary_consumer_groups;
-pub mod binary_consumer_offsets;
-pub mod binary_context;
-pub mod binary_message;
-pub mod binary_partitions;
-pub mod binary_personal_access_tokens;
-pub mod binary_segments;
-pub mod binary_streams;
-pub mod binary_system;
-pub mod binary_topics;
-pub mod binary_users;
-pub mod cli_command;
-pub mod utils;
+pub mod get_cluster_metadata;
diff --git a/core/binary_protocol/src/cli/mod.rs
b/core/binary_protocol/src/cli/mod.rs
index 53f1d9f1b..2395d92da 100644
--- a/core/binary_protocol/src/cli/mod.rs
+++ b/core/binary_protocol/src/cli/mod.rs
@@ -17,6 +17,7 @@
*/
pub mod binary_client;
+pub mod binary_cluster;
pub mod binary_consumer_groups;
pub mod binary_consumer_offsets;
pub mod binary_context;
diff --git a/core/binary_protocol/src/client/binary_clients/client.rs
b/core/binary_protocol/src/client/binary_clients/client.rs
index 64c2f87be..35a4aebe1 100644
--- a/core/binary_protocol/src/client/binary_clients/client.rs
+++ b/core/binary_protocol/src/client/binary_clients/client.rs
@@ -17,7 +17,7 @@
*/
use crate::{
- ConsumerGroupClient, ConsumerOffsetClient, MessageClient, PartitionClient,
+ ClusterClient, ConsumerGroupClient, ConsumerOffsetClient, MessageClient,
PartitionClient,
PersonalAccessTokenClient, SegmentClient, StreamClient, SystemClient,
TopicClient, UserClient,
};
use async_broadcast::Receiver;
@@ -30,7 +30,8 @@ use std::fmt::Debug;
/// Except the ping, login and get me, all the other methods require
authentication.
#[async_trait]
pub trait Client:
- SystemClient
+ ClusterClient
+ + SystemClient
+ UserClient
+ PersonalAccessTokenClient
+ StreamClient
diff --git a/core/binary_protocol/src/cli/mod.rs b/core/cli/src/args/cluster.rs
similarity index 66%
copy from core/binary_protocol/src/cli/mod.rs
copy to core/cli/src/args/cluster.rs
index 53f1d9f1b..8f2d8d121 100644
--- a/core/binary_protocol/src/cli/mod.rs
+++ b/core/cli/src/args/cluster.rs
@@ -16,17 +16,18 @@
* under the License.
*/
-pub mod binary_client;
-pub mod binary_consumer_groups;
-pub mod binary_consumer_offsets;
-pub mod binary_context;
-pub mod binary_message;
-pub mod binary_partitions;
-pub mod binary_personal_access_tokens;
-pub mod binary_segments;
-pub mod binary_streams;
-pub mod binary_system;
-pub mod binary_topics;
-pub mod binary_users;
-pub mod cli_command;
-pub mod utils;
+use crate::args::common::ListMode;
+use clap::{Args, Subcommand};
+
+#[derive(Debug, Clone, Subcommand)]
+pub(crate) enum ClusterAction {
+ /// Get cluster metadata
+ #[clap(visible_alias = "m")]
+ Metadata(ClusterMetadataArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub(crate) struct ClusterMetadataArgs {
+ #[clap(short, long, value_enum, default_value_t = ListMode::Table)]
+ pub(crate) list_mode: ListMode,
+}
diff --git a/core/cli/src/args/common.rs b/core/cli/src/args/common.rs
index 7f8e928c4..ddbc7539c 100644
--- a/core/cli/src/args/common.rs
+++ b/core/cli/src/args/common.rs
@@ -18,6 +18,7 @@
use clap::ValueEnum;
use iggy_binary_protocol::cli::binary_client::get_clients::GetClientsOutput;
+use
iggy_binary_protocol::cli::binary_cluster::get_cluster_metadata::GetClusterMetadataOutput;
use
iggy_binary_protocol::cli::binary_consumer_groups::get_consumer_groups::GetConsumerGroupsOutput;
use iggy_binary_protocol::cli::binary_context::get_contexts::GetContextsOutput;
use
iggy_binary_protocol::cli::binary_personal_access_tokens::get_personal_access_tokens::GetPersonalAccessTokensOutput;
@@ -32,6 +33,15 @@ pub(crate) enum ListMode {
List,
}
+impl From<ListMode> for GetClusterMetadataOutput {
+ fn from(mode: ListMode) -> Self {
+ match mode {
+ ListMode::Table => GetClusterMetadataOutput::Table,
+ ListMode::List => GetClusterMetadataOutput::List,
+ }
+ }
+}
+
impl From<ListMode> for GetStreamsOutput {
fn from(mode: ListMode) -> Self {
match mode {
diff --git a/core/cli/src/args/mod.rs b/core/cli/src/args/mod.rs
index 1d9d8c6e6..f90c20b89 100644
--- a/core/cli/src/args/mod.rs
+++ b/core/cli/src/args/mod.rs
@@ -30,6 +30,7 @@ use system::SnapshotArgs;
use crate::args::{
client::ClientAction,
+ cluster::ClusterAction,
consumer_group::ConsumerGroupAction,
consumer_offset::ConsumerOffsetAction,
context::ContextAction,
@@ -47,6 +48,7 @@ use crate::args::system::LoginArgs;
use self::user::UserAction;
pub(crate) mod client;
+pub(crate) mod cluster;
pub(crate) mod common;
pub(crate) mod consumer_group;
pub(crate) mod consumer_offset;
@@ -173,6 +175,9 @@ pub(crate) enum Command {
/// client operations
#[command(subcommand, visible_alias = "c")]
Client(ClientAction),
+ /// cluster operations
+ #[command(subcommand, visible_alias = "cl")]
+ Cluster(ClusterAction),
/// consumer group operations
#[command(subcommand, visible_alias = "g")]
ConsumerGroup(ConsumerGroupAction),
diff --git a/core/cli/src/main.rs b/core/cli/src/main.rs
index 21b60aaa1..bbdf1fa11 100644
--- a/core/cli/src/main.rs
+++ b/core/cli/src/main.rs
@@ -22,9 +22,10 @@ mod error;
mod logging;
use crate::args::{
- Command, IggyConsoleArgs, client::ClientAction,
consumer_group::ConsumerGroupAction,
- consumer_offset::ConsumerOffsetAction, permissions::PermissionsArgs,
- personal_access_token::PersonalAccessTokenAction, stream::StreamAction,
topic::TopicAction,
+ Command, IggyConsoleArgs, client::ClientAction, cluster::ClusterAction,
+ consumer_group::ConsumerGroupAction, consumer_offset::ConsumerOffsetAction,
+ permissions::PermissionsArgs,
personal_access_token::PersonalAccessTokenAction,
+ stream::StreamAction, topic::TopicAction,
};
use crate::credentials::IggyCredentials;
use crate::error::{CmdToolError, IggyCmdError};
@@ -46,6 +47,7 @@ use
iggy_binary_protocol::cli::binary_system::snapshot::GetSnapshotCmd;
use iggy_binary_protocol::cli::cli_command::{CliCommand, PRINT_TARGET};
use iggy_binary_protocol::cli::{
binary_client::{get_client::GetClientCmd, get_clients::GetClientsCmd},
+ binary_cluster::get_cluster_metadata::GetClusterMetadataCmd,
binary_consumer_groups::{
create_consumer_group::CreateConsumerGroupCmd,
delete_consumer_group::DeleteConsumerGroupCmd,
get_consumer_group::GetConsumerGroupCmd,
@@ -244,6 +246,11 @@ fn get_command(
Box::new(GetClientsCmd::new(list_args.list_mode.into()))
}
},
+ Command::Cluster(command) => match command {
+ ClusterAction::Metadata(args) => {
+ Box::new(GetClusterMetadataCmd::new(args.list_mode.into()))
+ }
+ },
Command::ConsumerGroup(command) => match command {
ConsumerGroupAction::Create(create_args) =>
Box::new(CreateConsumerGroupCmd::new(
create_args.stream_id.clone(),
diff --git a/core/integration/tests/cli/general/test_help_command.rs
b/core/integration/tests/cli/general/test_help_command.rs
index 9ae5fcb95..8cf0735b1 100644
--- a/core/integration/tests/cli/general/test_help_command.rs
+++ b/core/integration/tests/cli/general/test_help_command.rs
@@ -44,6 +44,7 @@ Commands:
pat personal access token operations
user user operations [aliases: u]
client client operations [aliases: c]
+ cluster cluster operations [aliases: cl]
consumer-group consumer group operations [aliases: g]
consumer-offset consumer offset operations [aliases: o]
message message operations [aliases: m]
diff --git a/core/integration/tests/cli/general/test_overview_command.rs
b/core/integration/tests/cli/general/test_overview_command.rs
index 6f48c4332..e351c039c 100644
--- a/core/integration/tests/cli/general/test_overview_command.rs
+++ b/core/integration/tests/cli/general/test_overview_command.rs
@@ -55,6 +55,7 @@ Commands:
pat personal access token operations
user user operations [aliases: u]
client client operations [aliases: c]
+ cluster cluster operations [aliases: cl]
consumer-group consumer group operations [aliases: g]
consumer-offset consumer offset operations [aliases: o]
message message operations [aliases: m]
diff --git a/core/integration/tests/cli/system/mod.rs
b/core/integration/tests/cli/system/mod.rs
index 53618b79c..a8c565891 100644
--- a/core/integration/tests/cli/system/mod.rs
+++ b/core/integration/tests/cli/system/mod.rs
@@ -20,6 +20,7 @@
// due to missing keyring support while running tests under cross
#[cfg(not(any(target_os = "macos", target_env = "musl")))]
mod test_cli_session_scenario;
+mod test_cluster_metadata_command;
#[cfg(not(any(target_os = "macos", target_env = "musl")))]
mod test_login_cmd;
mod test_login_command;
diff --git a/core/integration/tests/cli/system/test_cluster_metadata_command.rs
b/core/integration/tests/cli/system/test_cluster_metadata_command.rs
new file mode 100644
index 000000000..e4ae5791f
--- /dev/null
+++ b/core/integration/tests/cli/system/test_cluster_metadata_command.rs
@@ -0,0 +1,184 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use crate::cli::common::{IggyCmdCommand, IggyCmdTest, IggyCmdTestCase,
TestHelpCmd, USAGE_PREFIX};
+use assert_cmd::assert::Assert;
+use async_trait::async_trait;
+use iggy::prelude::Client;
+use predicates::str::{contains, starts_with};
+use serial_test::parallel;
+
+#[derive(Debug, Clone, Copy, Eq, PartialEq)]
+enum TestClusterMetadataCmdOutput {
+ Table,
+ List,
+}
+
+struct TestClusterMetadataCmd {
+ output_mode: TestClusterMetadataCmdOutput,
+}
+
+impl TestClusterMetadataCmd {
+ fn new(output_mode: TestClusterMetadataCmdOutput) -> Self {
+ Self { output_mode }
+ }
+}
+
+#[async_trait]
+impl IggyCmdTestCase for TestClusterMetadataCmd {
+ async fn prepare_server_state(&mut self, _client: &dyn Client) {}
+
+ fn get_command(&self) -> IggyCmdCommand {
+ let command = IggyCmdCommand::new().arg("cluster").arg("metadata");
+
+ match self.output_mode {
+ TestClusterMetadataCmdOutput::Table =>
command.with_env_credentials(),
+ TestClusterMetadataCmdOutput::List => command
+ .arg("--list-mode")
+ .arg("list")
+ .with_env_credentials(),
+ }
+ }
+
+ fn verify_command(&self, command_state: Assert) {
+ match self.output_mode {
+ TestClusterMetadataCmdOutput::Table => {
+ command_state
+ .success()
+ .stdout(starts_with(
+ "Executing get cluster metadata in table mode\n",
+ ))
+ .stdout(contains("Cluster name:"))
+ .stdout(contains("single-node"))
+ .stdout(contains("Name"))
+ .stdout(contains("IP"))
+ .stdout(contains("TCP"))
+ .stdout(contains("QUIC"))
+ .stdout(contains("HTTP"))
+ .stdout(contains("WebSocket"))
+ .stdout(contains("Role"))
+ .stdout(contains("Status"))
+ .stdout(contains("iggy-node"))
+ .stdout(contains("leader"))
+ .stdout(contains("healthy"));
+ }
+ TestClusterMetadataCmdOutput::List => {
+ command_state
+ .success()
+ .stdout(starts_with("Executing get cluster metadata in
list mode\n"))
+ .stdout(contains("Cluster name:"))
+ .stdout(contains("single-node"))
+ .stdout(contains("iggy-node"))
+ .stdout(contains("leader"))
+ .stdout(contains("healthy"));
+ }
+ }
+ }
+
+ async fn verify_server_state(&self, _client: &dyn Client) {}
+}
+
+#[tokio::test]
+#[parallel]
+pub async fn should_be_successful_table_mode() {
+ let mut iggy_cmd_test = IggyCmdTest::default();
+ iggy_cmd_test.setup().await;
+ iggy_cmd_test
+ .execute_test(TestClusterMetadataCmd::new(
+ TestClusterMetadataCmdOutput::Table,
+ ))
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+pub async fn should_be_successful_list_mode() {
+ let mut iggy_cmd_test = IggyCmdTest::default();
+ iggy_cmd_test.setup().await;
+ iggy_cmd_test
+ .execute_test(TestClusterMetadataCmd::new(
+ TestClusterMetadataCmdOutput::List,
+ ))
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+pub async fn should_help_match() {
+ let mut iggy_cmd_test = IggyCmdTest::help_message();
+ iggy_cmd_test
+ .execute_test_for_help_command(TestHelpCmd::new(
+ vec!["cluster", "metadata", "--help"],
+ format!(
+ r#"Get cluster metadata
+
+{USAGE_PREFIX} cluster metadata [OPTIONS]
+
+Options:
+ -l, --list-mode <LIST_MODE> [default: table] [possible values: table, list]
+ -h, --help Print help
+"#,
+ ),
+ ))
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+pub async fn should_short_help_match() {
+ let mut iggy_cmd_test = IggyCmdTest::help_message();
+ iggy_cmd_test
+ .execute_test_for_help_command(TestHelpCmd::new(
+ vec!["cluster", "metadata", "-h"],
+ format!(
+ r#"Get cluster metadata
+
+{USAGE_PREFIX} cluster metadata [OPTIONS]
+
+Options:
+ -l, --list-mode <LIST_MODE> [default: table] [possible values: table, list]
+ -h, --help Print help
+"#,
+ ),
+ ))
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+pub async fn should_cluster_help_match() {
+ let mut iggy_cmd_test = IggyCmdTest::help_message();
+ iggy_cmd_test
+ .execute_test_for_help_command(TestHelpCmd::new(
+ vec!["cluster", "--help"],
+ format!(
+ r#"cluster operations
+
+{USAGE_PREFIX} cluster <COMMAND>
+
+Commands:
+ metadata Get cluster metadata [aliases: m]
+ help Print this message or the help of the given subcommand(s)
+
+Options:
+ -h, --help Print help
+"#,
+ ),
+ ))
+ .await;
+}