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 0000000000..4ed1d95cab --- /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/binary_cluster/mod.rs b/core/binary_protocol/src/cli/binary_cluster/mod.rs new file mode 100644 index 0000000000..e8f9e216c2 --- /dev/null +++ b/core/binary_protocol/src/cli/binary_cluster/mod.rs @@ -0,0 +1,19 @@ +/* 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. + */ + +pub mod get_cluster_metadata; diff --git a/core/binary_protocol/src/cli/mod.rs b/core/binary_protocol/src/cli/mod.rs index 53f1d9f1b7..2395d92daf 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 64c2f87be1..35a4aebe1f 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/cli/src/args/cluster.rs b/core/cli/src/args/cluster.rs new file mode 100644 index 0000000000..8f2d8d1215 --- /dev/null +++ b/core/cli/src/args/cluster.rs @@ -0,0 +1,33 @@ +/* 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::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 7f8e928c47..ddbc7539c6 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 for GetClusterMetadataOutput { + fn from(mode: ListMode) -> Self { + match mode { + ListMode::Table => GetClusterMetadataOutput::Table, + ListMode::List => GetClusterMetadataOutput::List, + } + } +} + impl From 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 1d9d8c6e65..f90c20b893 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 21b60aaa15..bbdf1fa11a 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 9ae5fcb95a..8cf0735b10 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 6f48c43327..e351c039c7 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 53618b79cf..a8c5658914 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 0000000000..e4ae5791f2 --- /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 [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 [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 + +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; +}