diff --git a/Cargo.lock b/Cargo.lock index fca1cab21f..d6c3ead858 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1330,9 +1330,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb" dependencies = [ "android-tzdata", "iana-time-zone", @@ -1340,7 +1340,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.48.1", + "windows-targets 0.52.0", ] [[package]] @@ -1405,9 +1405,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" +checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651" dependencies = [ "clap_builder", "clap_derive", @@ -1415,9 +1415,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", @@ -1571,7 +1571,7 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" dependencies = [ - "encode_unicode", + "encode_unicode 0.3.6", "lazy_static", "libc", "windows-sys 0.45.0", @@ -1647,11 +1647,21 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpp_demangle" @@ -2411,6 +2421,12 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.32" @@ -3019,6 +3035,21 @@ dependencies = [ "digest", ] +[[package]] +name = "horaectl" +version = "2.0.0" +dependencies = [ + "anyhow", + "chrono", + "clap", + "lazy_static", + "prettytable", + "reqwest", + "serde", + "shell-words", + "tokio", +] + [[package]] name = "horaedb" version = "2.0.0" @@ -3185,15 +3216,16 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.23.2" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ + "futures-util", "http", "hyper", - "rustls 0.20.8", + "rustls 0.21.6", "tokio", - "tokio-rustls 0.23.4", + "tokio-rustls 0.24.1", ] [[package]] @@ -3551,9 +3583,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.61" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" dependencies = [ "wasm-bindgen", ] @@ -5084,6 +5116,20 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "prettytable" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46480520d1b77c9a3482d39939fcf96831537a250ec62d4fd8fbdf8e0302e781" +dependencies = [ + "csv", + "encode_unicode 1.0.0", + "is-terminal", + "lazy_static", + "term", + "unicode-width", +] + [[package]] name = "proc-macro-crate" version = "0.1.5" @@ -5815,9 +5861,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.16" +version = "0.11.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254" +checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" dependencies = [ "base64 0.21.0", "bytes", @@ -5836,13 +5882,15 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.20.8", + "rustls 0.21.6", "rustls-pemfile 1.0.2", "serde", "serde_json", "serde_urlencoded", + "sync_wrapper", + "system-configuration", "tokio", - "tokio-rustls 0.23.4", + "tokio-rustls 0.24.1", "tokio-util", "tower-service", "url", @@ -5850,7 +5898,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 0.22.6", + "webpki-roots 0.25.4", "winreg", ] @@ -6268,9 +6316,9 @@ checksum = "e6b44e8fc93a14e66336d230954dda83d18b4605ccace8fe09bc7514a71ad0bc" [[package]] name = "serde" -version = "1.0.159" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" dependencies = [ "serde_derive", ] @@ -6286,9 +6334,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.159" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" dependencies = [ "proc-macro2", "quote", @@ -6432,6 +6480,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "shlex" version = "1.3.0" @@ -6879,6 +6933,27 @@ dependencies = [ "windows 0.52.0", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system_catalog" version = "2.0.0" @@ -7220,6 +7295,16 @@ dependencies = [ "webpki", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.6", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.25.0" @@ -7872,9 +7957,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -7882,24 +7967,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.48", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.34" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" +checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -7909,9 +7994,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7919,28 +8004,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" [[package]] name = "wasm-streams" -version = "0.2.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bbae3363c08332cadccd13b67db371814cd214c2524020932f0804b8cf7c078" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" dependencies = [ "futures-util", "js-sys", @@ -7951,9 +8036,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.61" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" dependencies = [ "js-sys", "wasm-bindgen", @@ -7971,21 +8056,18 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.22.6" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338" dependencies = [ - "webpki", + "rustls-webpki 0.100.2", ] [[package]] name = "webpki-roots" -version = "0.23.1" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338" -dependencies = [ - "rustls-webpki 0.100.2", -] +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "which" @@ -8323,11 +8405,12 @@ dependencies = [ [[package]] name = "winreg" -version = "0.10.1" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "winapi", + "cfg-if 1.0.0", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ec6fcd4e23..77277e084f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ license = "Apache-2.0" resolver = "2" # In alphabetical order members = [ + "horaectl", "integration_tests", "integration_tests/sdk/rust", "src/analytic_engine", @@ -89,6 +90,7 @@ arrow = { version = "49.0.0", features = ["prettyprint"] } arrow_ipc = { version = "49.0.0" } arrow_ext = { path = "src/components/arrow_ext" } analytic_engine = { path = "src/analytic_engine" } +anyhow = { version = "1.0" } arena = { path = "src/components/arena" } async-stream = "0.3.4" async-trait = "0.1.72" @@ -101,7 +103,7 @@ catalog_impls = { path = "src/catalog_impls" } horaedbproto = { git = "https://github.com/apache/incubator-horaedb-proto.git", rev = "19ece8f771fc0b3e8e734072cc3d8040de6c74cb" } codec = { path = "src/components/codec" } chrono = "0.4" -clap = "4.5.1" +clap = { version = "4.5.1", features = ["derive"] } clru = "0.6.1" cluster = { path = "src/cluster" } criterion = "0.5" diff --git a/horaectl/Cargo.toml b/horaectl/Cargo.toml new file mode 100644 index 0000000000..580c0e7946 --- /dev/null +++ b/horaectl/Cargo.toml @@ -0,0 +1,39 @@ +# 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. + +[package] +name = "horaectl" + +[package.license] +workspace = true + +[package.version] +workspace = true + +[package.edition] +workspace = true + +[dependencies] +anyhow = { workspace = true, features = ["backtrace"] } +chrono = { workspace = true } +clap = { workspace = true, features = ["env", "derive"] } +lazy_static = { workspace = true } +prettytable = "0.10.0" +reqwest = { workspace = true } +serde = { workspace = true } +shell-words = "1.1.0" +tokio = { workspace = true } diff --git a/horaectl/src/cmd/cluster.rs b/horaectl/src/cmd/cluster.rs new file mode 100644 index 0000000000..cd22c0ccc3 --- /dev/null +++ b/horaectl/src/cmd/cluster.rs @@ -0,0 +1,67 @@ +// 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 anyhow::Result; +use clap::Subcommand; + +use crate::operation::cluster::ClusterOp; + +#[derive(Subcommand)] +pub enum ClusterCommand { + /// List cluster + List, + + /// Diagnose cluster + Diagnose, + + /// Schedule cluster + Schedule { + #[clap(subcommand)] + cmd: Option, + }, +} + +#[derive(Subcommand)] +pub enum ScheduleCommand { + /// Get the schedule status + Get, + + /// Enable schedule + On, + + /// Disable schedule + Off, +} + +pub async fn run(cmd: ClusterCommand) -> Result<()> { + let op = ClusterOp::try_new()?; + match cmd { + ClusterCommand::List => op.list().await, + ClusterCommand::Diagnose => op.diagnose().await, + ClusterCommand::Schedule { cmd } => { + if let Some(cmd) = cmd { + match cmd { + ScheduleCommand::Get => op.get_schedule_status().await, + ScheduleCommand::On => op.update_schedule_status(true).await, + ScheduleCommand::Off => op.update_schedule_status(false).await, + } + } else { + op.get_schedule_status().await + } + } + } +} diff --git a/horaectl/src/cmd/mod.rs b/horaectl/src/cmd/mod.rs new file mode 100644 index 0000000000..5906ef1822 --- /dev/null +++ b/horaectl/src/cmd/mod.rs @@ -0,0 +1,140 @@ +// 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. + +mod cluster; +use std::{io, io::Write}; + +use anyhow::Result; +use clap::{Args, Parser, Subcommand}; + +use crate::{ + cmd::cluster::ClusterCommand, + util::{CLUSTER_NAME, META_ADDR}, +}; + +#[derive(Parser)] +#[clap(name = "horaectl")] +#[clap(about = "HoraeCTL is a command line tool for HoraeDB", version)] +pub struct App { + #[clap(flatten)] + pub global_opts: GlobalOpts, + + /// Enter interactive mode + #[clap(short, long, default_value_t = false)] + pub interactive: bool, + + #[clap(subcommand)] + pub command: Option, +} + +#[derive(Debug, Args)] +pub struct GlobalOpts { + /// Meta addr + #[clap( + short, + long = "meta", + global = true, + env = "HORAECTL_META_ADDR", + default_value = "127.0.0.1:8080" + )] + pub meta_addr: String, + + /// Cluster name + #[clap( + short, + long = "cluster", + global = true, + env = "HORAECTL_CLUSTER", + default_value = "defaultCluster" + )] + pub cluster_name: String, +} + +#[derive(Subcommand)] +pub enum SubCommand { + /// Operations on cluster + #[clap(alias = "c")] + Cluster { + #[clap(subcommand)] + commands: ClusterCommand, + }, +} + +pub async fn run_command(cmd: SubCommand) -> Result<()> { + match cmd { + SubCommand::Cluster { commands } => cluster::run(commands).await, + } +} + +pub async fn repl_loop() { + loop { + print_prompt( + META_ADDR.lock().unwrap().as_str(), + CLUSTER_NAME.lock().unwrap().as_str(), + ); + + let args = match read_args() { + Ok(args) => args, + Err(e) => { + println!("Read input failed, err:{}", e); + continue; + } + }; + + if let Some(cmd) = args.get(1) { + if ["quit", "exit", "q"].iter().any(|v| v == cmd) { + break; + } + } + + match App::try_parse_from(args) { + Ok(horaectl) => { + if let Some(cmd) = horaectl.command { + if let Err(e) = match cmd { + SubCommand::Cluster { commands } => cluster::run(commands).await, + } { + println!("Run command failed, err:{e}"); + } + } + } + Err(e) => { + println!("Parse command failed, err:{e}"); + } + } + } +} + +fn read_args() -> Result, String> { + io::stdout().flush().unwrap(); + let mut input = String::new(); + io::stdin() + .read_line(&mut input) + .map_err(|e| e.to_string())?; + + let input = input.trim(); + if input.is_empty() { + return Err("No arguments provided".into()); + } + + let mut args = vec!["horaectl".to_string()]; + args.extend(shell_words::split(input).map_err(|e| e.to_string())?); + Ok(args) +} + +fn print_prompt(address: &str, cluster: &str) { + print!("{}({}) > ", address, cluster); +} diff --git a/horaectl/src/main.rs b/horaectl/src/main.rs new file mode 100644 index 0000000000..24658ab626 --- /dev/null +++ b/horaectl/src/main.rs @@ -0,0 +1,54 @@ +// 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. + +mod cmd; +mod operation; +mod util; + +use clap::{CommandFactory, Parser}; + +use crate::{ + cmd::{repl_loop, run_command, App}, + util::{CLUSTER_NAME, META_ADDR}, +}; + +#[tokio::main] +async fn main() { + let app = App::parse(); + { + let mut meta_addr = META_ADDR.lock().unwrap(); + *meta_addr = app.global_opts.meta_addr; + } + { + let mut cluster_name = CLUSTER_NAME.lock().unwrap(); + *cluster_name = app.global_opts.cluster_name; + } + + if app.interactive { + repl_loop().await; + return; + } + + if let Some(cmd) = app.command { + if let Err(e) = run_command(cmd).await { + println!("Run command failed, err:{e}"); + std::process::exit(1); + } + } else { + App::command().print_help().expect("print help failed"); + } +} diff --git a/horaectl/src/operation/cluster.rs b/horaectl/src/operation/cluster.rs new file mode 100644 index 0000000000..709d44be26 --- /dev/null +++ b/horaectl/src/operation/cluster.rs @@ -0,0 +1,147 @@ +// 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 std::time::Duration; + +use anyhow::Result; +use prettytable::row; +use reqwest::Client; + +use crate::{ + operation::{ + ClusterResponse, DiagnoseShardResponse, EnableScheduleRequest, EnableScheduleResponse, + }, + util::{ + format_time_milli, table_writer, API, CLUSTERS, CLUSTERS_DIAGNOSE_HEADER, + CLUSTERS_ENABLE_SCHEDULE_HEADER, CLUSTERS_LIST_HEADER, CLUSTER_NAME, DEBUG, HTTP, + META_ADDR, + }, +}; + +fn list_url() -> String { + HTTP.to_string() + META_ADDR.lock().unwrap().as_str() + API + CLUSTERS +} + +fn diagnose_url() -> String { + HTTP.to_string() + + META_ADDR.lock().unwrap().as_str() + + DEBUG + + "/diagnose" + + "/" + + CLUSTER_NAME.lock().unwrap().as_str() + + "/shards" +} + +fn schedule_url() -> String { + HTTP.to_string() + + META_ADDR.lock().unwrap().as_str() + + DEBUG + + CLUSTERS + + "/" + + CLUSTER_NAME.lock().unwrap().as_str() + + "/enableSchedule" +} + +pub struct ClusterOp { + http_client: Client, +} + +impl ClusterOp { + pub fn try_new() -> Result { + let hc = Client::builder() + .timeout(Duration::from_secs(30)) + .user_agent("horaectl") + .build()?; + + Ok(Self { http_client: hc }) + } + + pub async fn list(&self) -> Result<()> { + let res = self.http_client.get(list_url()).send().await?; + let response: ClusterResponse = res.json().await?; + + let mut table = table_writer(&CLUSTERS_LIST_HEADER); + for cluster in response.data { + table.add_row(row![ + cluster.id, + cluster.name, + cluster.shard_total.to_string(), + cluster.topology_type, + cluster.procedure_executing_batch_size.to_string(), + format_time_milli(cluster.created_at), + format_time_milli(cluster.modified_at) + ]); + } + table.printstd(); + + Ok(()) + } + + pub async fn diagnose(&self) -> Result<()> { + let res = self.http_client.get(diagnose_url()).send().await?; + let response: DiagnoseShardResponse = res.json().await?; + let mut table = table_writer(&CLUSTERS_DIAGNOSE_HEADER); + table.add_row(row![response + .data + .unregistered_shards + .iter() + .map(|shard_id| shard_id.to_string()) + .collect::>() + .join(", ")]); + for (shard_id, data) in response.data.unready_shards { + table.add_row(row!["", shard_id, data.node_name, data.status]); + } + table.printstd(); + + Ok(()) + } + + pub async fn get_schedule_status(&self) -> Result<()> { + let res = self.http_client.get(schedule_url()).send().await?; + let response: EnableScheduleResponse = res.json().await?; + let mut table = table_writer(&CLUSTERS_ENABLE_SCHEDULE_HEADER); + let row = match response.data { + Some(data) => row![data], + None => row!["topology should in dynamic mode"], + }; + table.add_row(row); + table.printstd(); + + Ok(()) + } + + pub async fn update_schedule_status(&self, enable: bool) -> Result<()> { + let request = EnableScheduleRequest { enable }; + + let res = self + .http_client + .put(schedule_url()) + .json(&request) + .send() + .await?; + let response: EnableScheduleResponse = res.json().await?; + let mut table = table_writer(&CLUSTERS_ENABLE_SCHEDULE_HEADER); + let row = match response.data { + Some(data) => row![data], + None => row!["topology should in dynamic mode"], + }; + table.add_row(row); + table.printstd(); + + Ok(()) + } +} diff --git a/horaectl/src/operation/mod.rs b/horaectl/src/operation/mod.rs new file mode 100644 index 0000000000..f872346700 --- /dev/null +++ b/horaectl/src/operation/mod.rs @@ -0,0 +1,80 @@ +// 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 cluster; + +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +#[derive(Deserialize, Debug)] +pub struct Cluster { + #[serde(rename = "ID")] + id: u32, + #[serde(rename = "Name")] + name: String, + #[serde(rename = "ShardTotal")] + shard_total: u32, + #[serde(rename = "TopologyType")] + topology_type: String, + #[serde(rename = "ProcedureExecutingBatchSize")] + procedure_executing_batch_size: u32, + #[serde(rename = "CreatedAt")] + created_at: i64, + #[serde(rename = "ModifiedAt")] + modified_at: i64, +} + +#[derive(Deserialize, Debug)] +pub struct ClusterResponse { + #[allow(unused)] + status: String, + data: Vec, +} + +#[derive(Deserialize, Debug)] +pub struct DiagnoseShardStatus { + #[serde(rename = "nodeName")] + node_name: String, + status: String, +} + +#[derive(Deserialize, Debug)] +pub struct DiagnoseShard { + #[serde(rename = "unregisteredShards")] + unregistered_shards: Vec, + #[serde(rename = "unreadyShards")] + unready_shards: HashMap, +} + +#[derive(Deserialize, Debug)] +pub struct DiagnoseShardResponse { + #[allow(unused)] + status: String, + data: DiagnoseShard, +} + +#[derive(Serialize)] +pub struct EnableScheduleRequest { + enable: bool, +} + +#[derive(Deserialize)] +pub struct EnableScheduleResponse { + #[allow(unused)] + status: String, + data: Option, +} diff --git a/horaectl/src/util/mod.rs b/horaectl/src/util/mod.rs new file mode 100644 index 0000000000..d6a4311365 --- /dev/null +++ b/horaectl/src/util/mod.rs @@ -0,0 +1,60 @@ +// 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 std::sync::Mutex; + +use chrono::{TimeZone, Utc}; +use lazy_static::lazy_static; +use prettytable::{Cell, Row, Table}; + +lazy_static! { + pub static ref META_ADDR: Mutex = Mutex::new(String::new()); + pub static ref CLUSTER_NAME: Mutex = Mutex::new(String::new()); +} + +pub const HTTP: &str = "http://"; +pub const API: &str = "/api/v1"; +pub const DEBUG: &str = "/debug"; +pub const CLUSTERS: &str = "/clusters"; +pub static CLUSTERS_LIST_HEADER: [&str; 7] = [ + "ID", + "Name", + "ShardTotal", + "TopologyType", + "ProcedureExecutingBatchSize", + "CreatedAt", + "ModifiedAt", +]; +pub static CLUSTERS_DIAGNOSE_HEADER: [&str; 4] = [ + "unregistered_shards", + "unready_shards:shard_id", + "unready_shards:node_name", + "unready_shards:status", +]; +pub static CLUSTERS_ENABLE_SCHEDULE_HEADER: [&str; 1] = ["enable_schedule"]; + +pub fn table_writer(header: &[&str]) -> Table { + let mut table = Table::new(); + let header_row = Row::from_iter(header.iter().map(|&entry| Cell::new(entry))); + table.add_row(header_row); + table +} + +pub fn format_time_milli(milli: i64) -> String { + let datetime = Utc.timestamp_millis_opt(milli).single().unwrap(); + datetime.format("%Y-%m-%d %H:%M:%S%.3f").to_string() +} diff --git a/src/tools/Cargo.toml b/src/tools/Cargo.toml index fb3910665e..1a3231cb8f 100644 --- a/src/tools/Cargo.toml +++ b/src/tools/Cargo.toml @@ -32,7 +32,7 @@ workspace = true [dependencies] analytic_engine = { workspace = true } -anyhow = { version = "1.0", features = ["backtrace"] } +anyhow = { workspace = true, features = ["backtrace"] } clap = { workspace = true, features = ["derive"] } common_types = { workspace = true } futures = { workspace = true }