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;
+}

Reply via email to