diff --git a/CHANGELOG.md b/CHANGELOG.md index e8bb155ae81dc39e58d685d808777c8d60296b78..e90d3fbd86ac45bf2b9042808a30e3ae02285d7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,12 @@ with the `YY.0M.MICRO` scheme. +### CLI + +- New command `picodata admin` to connect to picodata instance via unix socket under the admin account. + +- SQL by default in `picodata connect`. Lua language is deprecated in `picodata connect`. + ## [23.12.0] - 2023-12-08 ### Features diff --git a/src/cli/admin.rs b/src/cli/admin.rs index 88ad7c4b878a9c9bc0f77262ad6e2a06891f0531..e634ce42bbad7a3b0a00fdd15f1594761d112b60 100644 --- a/src/cli/admin.rs +++ b/src/cli/admin.rs @@ -3,51 +3,10 @@ use crate::{ util::{unwrap_or_terminate, validate_and_complete_unix_socket_path}, }; -use super::args::{self, Address}; +use super::args; -pub(crate) fn get_password_from_file(path: &str) -> Result<String, String> { - let content = std::fs::read_to_string(path).map_err(|e| { - format!(r#"can't read password from password file by "{path}", reason: {e}"#) - })?; - - let password = content - .lines() - .next() - .ok_or("Empty password file".to_string())? - .trim(); - - if password.is_empty() { - return Ok(String::new()); - } - - Ok(password.into()) -} - -fn connect_and_start_interacitve_console(args: args::Connect) -> Result<(), String> { - let endpoint = if args.address_as_socket { - validate_and_complete_unix_socket_path(&args.address)? - } else { - let address = args - .address - .parse::<Address>() - .map_err(|msg| format!("invalid format of address argument: {msg}"))?; - let user = address.user.unwrap_or(args.user); - - let password = if user == args::DEFAULT_USERNAME { - String::new() - } else if let Some(path) = args.password_file { - get_password_from_file(&path)? - } else { - let prompt = format!("Enter password for {user}: "); - crate::util::prompt_password(&prompt) - .map_err(|e| format!("\nFailed to prompt for a password: {e}"))? - }; - - format!( - "{}:{}@{}:{}?auth_type={}", - user, password, address.host, address.port, args.auth_method - ) - }; +fn connect_and_start_interacitve_console(args: args::Admin) -> Result<(), String> { + let endpoint = validate_and_complete_unix_socket_path(&args.socket_path)?; tarantool::lua_state() .exec_with( @@ -60,11 +19,11 @@ fn connect_and_start_interacitve_console(args: args::Connect) -> Result<(), Stri Ok(()) } -pub fn main(args: args::Connect) -> ! { +pub fn main(args: args::Admin) -> ! { let rc = tarantool_main!( args.tt_args().unwrap(), callback_data: args, - callback_data_type: args::Connect, + callback_data_type: args::Admin, callback_body: { unwrap_or_terminate(connect_and_start_interacitve_console(args)); std::process::exit(0) diff --git a/src/cli/args.rs b/src/cli/args.rs index eb05656381bcb23aa0d3549653c018daeb0d1141..a7b260841c54e8044ef56f006adb8cb558f84bb8 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -19,7 +19,7 @@ pub enum Picodata { Expel(Expel), Test(Test), Connect(Connect), - Sql(ConnectSql), + Admin(Admin), } //////////////////////////////////////////////////////////////////////////////// @@ -137,7 +137,7 @@ pub struct Run { #[clap(long, value_name = "PATH", env = "PICODATA_CONSOLE_SOCK")] /// Unix socket for the interactive console to connect using - /// `picodata connect --unix`. Unlike connecting to a + /// `picodata admin`. Unlike connecting to a /// `--listen` address, console communication occurs in plain text /// and always operates under the admin account. pub console_sock: Option<String>, @@ -339,55 +339,11 @@ fn try_parse_kv_uppercase(s: &str) -> Result<(Uppercase, Uppercase), String> { //////////////////////////////////////////////////////////////////////////////// #[derive(Debug, Parser)] -#[clap(about = "Connect a Picodata instance and start interactive Lua console")] -pub struct Connect { - #[clap( - short = 'u', - long = "user", - value_name = "USER", - default_value = DEFAULT_USERNAME, - env = "PICODATA_USER" - )] - /// The username to connect with. Ignored if provided in `ADDRESS`. - pub user: String, - - #[clap( - short = 'a', - long = "auth-type", - value_name = "METHOD", - default_value = AuthMethod::ChapSha1.as_str(), - )] - /// The preferred authentication method. - pub auth_method: AuthMethod, - - #[clap(long = "unix")] - /// Treat ADDRESS as a unix socket path. - pub address_as_socket: bool, - - #[clap(value_name = "ADDRESS")] - /// Picodata instance address to connect. Format: - /// `[user@][host][:port]` or `--unix <PATH>`. - pub address: String, - - #[clap(long, env = "PICODATA_PASSWORD_FILE")] - /// Path to a plain-text file with a password. - /// If this option isn't provided, the password is prompted from the terminal. - pub password_file: Option<String>, -} - -impl Connect { - /// Get the arguments that will be passed to `tarantool_main` - pub fn tt_args(&self) -> Result<Vec<CString>, String> { - Ok(vec![current_exe()?]) - } -} - -#[derive(Debug, Parser)] -#[clap(about = "Connect a Picodata instance and start interactive SQL console")] +#[clap(about = "Connect to a Picodata instance and start interactive SQL console")] #[clap( - long_about = "Connect a Picodata instance and start interactive SQL console + long_about = "Connect to a Picodata instance and start interactive SQL console -In addition to running sql queries picodata sql supports simple meta commands. +In addition to running sql queries picodata connect supports simple meta commands. Anything you enter in picodata sql that begins with an unquoted backslash is a meta-command that is processed by the cli itself. These commands make cli more @@ -401,12 +357,12 @@ Currently there is only one such command, but other ones are expected to appear. content is treated as a SQL query and attempted to be executed. " )] -pub struct ConnectSql { +pub struct Connect { #[clap( short = 'u', long = "user", value_name = "USER", - default_value = "guest", + default_value = DEFAULT_USERNAME, env = "PICODATA_USER" )] /// The username to connect with. Ignored if provided in `ADDRESS`. @@ -422,8 +378,9 @@ pub struct ConnectSql { pub auth_method: AuthMethod, #[clap(value_name = "ADDRESS")] - /// Picodata instance address. Format: `[user@][host][:port]` - pub address: Address, + /// Picodata instance address to connect. Format: + /// `[user@][host][:port]`. + pub address: String, #[clap(long, env = "PICODATA_PASSWORD_FILE")] /// Path to a plain-text file with a password. @@ -431,7 +388,22 @@ pub struct ConnectSql { pub password_file: Option<String>, } -impl ConnectSql { +impl Connect { + /// Get the arguments that will be passed to `tarantool_main` + pub fn tt_args(&self) -> Result<Vec<CString>, String> { + Ok(vec![current_exe()?]) + } +} + +#[derive(Debug, Parser)] +#[clap(about = "Connect to admin console of a Picodata instance")] +pub struct Admin { + #[clap(value_name = "PATH")] + /// Picodata instance admin socket path to connect. + pub socket_path: String, +} + +impl Admin { /// Get the arguments that will be passed to `tarantool_main` pub fn tt_args(&self) -> Result<Vec<CString>, String> { Ok(vec![current_exe()?]) diff --git a/src/cli/connect.rs b/src/cli/connect.rs index d0caea8b341980c7ba505ec0829782fa0569946d..d2fcd793239f1c5241b32df1013f5566d10cb95c 100644 --- a/src/cli/connect.rs +++ b/src/cli/connect.rs @@ -1,6 +1,7 @@ use std::fmt::Display; use std::ops::ControlFlow; use std::path::Path; +use std::str::FromStr; use std::{env, fs, io, process}; use comfy_table::{ContentArrangement, Table}; @@ -12,8 +13,25 @@ use tarantool::network::{client, AsClient, Client, Config}; use crate::tarantool_main; use crate::util::unwrap_or_terminate; -use super::args::{self, DEFAULT_USERNAME}; -use super::connect::get_password_from_file; +use super::args::{self, Address, DEFAULT_USERNAME}; + +pub(crate) fn get_password_from_file(path: &str) -> Result<String, String> { + let content = std::fs::read_to_string(path).map_err(|e| { + format!(r#"can't read password from password file by "{path}", reason: {e}"#) + })?; + + let password = content + .lines() + .next() + .ok_or("Empty password file".to_string())? + .trim(); + + if password.is_empty() { + return Ok(String::new()); + } + + Ok(password.into()) +} #[derive(serde::Serialize, serde::Deserialize, Debug)] struct ColDesc { @@ -99,7 +117,7 @@ enum SqlReplError { const HISTORY_FILE_NAME: &str = ".picodata_history"; -async fn sql_repl_main(args: args::ConnectSql) { +async fn sql_repl_main(args: args::Connect) { unwrap_or_terminate(sql_repl(args).await); std::process::exit(0) } @@ -140,8 +158,9 @@ fn handle_special_sequence(line: &str) -> Result<ControlFlow<String>, SqlReplErr Ok(ControlFlow::Break(line)) } -async fn sql_repl(args: args::ConnectSql) -> Result<(), SqlReplError> { - let user = args.address.user.as_ref().unwrap_or(&args.user).clone(); +async fn sql_repl(args: args::Connect) -> Result<(), SqlReplError> { + let address = unwrap_or_terminate(Address::from_str(&args.address)); + let user = address.user.as_ref().unwrap_or(&args.user).clone(); let password = if user == DEFAULT_USERNAME { String::new() @@ -158,8 +177,8 @@ async fn sql_repl(args: args::ConnectSql) -> Result<(), SqlReplError> { }; let client = Client::connect_with_config( - &args.address.host, - args.address.port.parse().unwrap(), + &address.host, + address.port.parse().unwrap(), Config { creds: Some((user, password)), }, @@ -225,11 +244,11 @@ async fn sql_repl(args: args::ConnectSql) -> Result<(), SqlReplError> { Ok(()) } -pub fn main(args: args::ConnectSql) -> ! { +pub fn main(args: args::Connect) -> ! { let rc = tarantool_main!( args.tt_args().unwrap(), callback_data: (args,), - callback_data_type: (args::ConnectSql,), + callback_data_type: (args::Connect,), callback_body: { ::tarantool::fiber::block_on(sql_repl_main(args)) } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index c97dfe5f5a484948bb6356f419b28fbb7e39416d..8b4eedd790345b3c52231d971b7d13670c9fbfbd 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,9 +1,9 @@ +pub mod admin; pub mod args; pub mod connect; pub mod expel; pub mod init_cfg; pub mod run; -pub mod sql; pub mod tarantool; pub mod test; diff --git a/src/main.rs b/src/main.rs index 020102f9ea2f07663082b2a689c986d5a92520a0..d05e9e406801386f9dc727a853ff688d8afb062f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,6 @@ fn main() -> ! { Picodata::Tarantool(args) => cli::tarantool::main(args), Picodata::Expel(args) => cli::expel::main(args), Picodata::Connect(args) => cli::connect::main(args), - Picodata::Sql(args) => cli::sql::main(args), + Picodata::Admin(args) => cli::admin::main(args), } } diff --git a/test/int/test_cli_connect.py b/test/int/test_cli_connect.py index 93eed2dfa544de0b9b37451b2e4d8b66165a5cda..37f2a1f0743005151918969dfadfe318b8f46e46 100644 --- a/test/int/test_cli_connect.py +++ b/test/int/test_cli_connect.py @@ -29,15 +29,7 @@ def test_connect_testuser(i1: Instance): cli.expect_exact("Enter password for testuser: ") cli.sendline("testpass") - cli.expect_exact(f"connected to {i1.host}:{i1.port}") - cli.expect_exact(f"{i1.host}:{i1.port}>") - - cli.sendline("\\set language lua") - cli.sendline("box.session.user()") - cli.expect_exact("---\r\n") - cli.expect_exact("- testuser\r\n") - cli.expect_exact("...\r\n") - cli.expect_exact("\r\n") + cli.expect_exact("picosql :)") eprint("^D") cli.sendcontrol("d") @@ -56,15 +48,7 @@ def test_connect_user_host_port(i1: Instance): cli.expect_exact("Enter password for testuser: ") cli.sendline("testpass") - cli.expect_exact(f"connected to {i1.host}:{i1.port}") - cli.expect_exact(f"{i1.host}:{i1.port}>") - - cli.sendline("\\set language lua") - cli.sendline("box.session.user()") - cli.expect_exact("---\r\n") - cli.expect_exact("- testuser\r\n") - cli.expect_exact("...\r\n") - cli.expect_exact("\r\n") + cli.expect_exact("picosql :)") eprint("^D") cli.sendcontrol("d") @@ -80,15 +64,7 @@ def test_connect_guest(i1: Instance): ) cli.logfile = sys.stdout - cli.expect_exact(f"connected to {i1.host}:{i1.port}") - cli.expect_exact(f"{i1.host}:{i1.port}>") - - cli.sendline("\\set language lua") - cli.sendline("box.session.user()") - cli.expect_exact("---\r\n") - cli.expect_exact("- guest\r\n") - cli.expect_exact("...\r\n") - cli.expect_exact("\r\n") + cli.expect_exact("picosql :)") eprint("^D") cli.sendcontrol("d") @@ -124,7 +100,7 @@ def test_wrong_pass(i1: Instance): cli.expect_exact("Enter password for testuser: ") cli.sendline("badpass") - cli.expect_exact("Connection is not established") + cli.expect_exact("service responded with error") cli.expect_exact("User not found or supplied credentials are invalid") cli.expect_exact(pexpect.EOF) @@ -142,8 +118,8 @@ def test_connection_refused(binary_path: str): cli.expect_exact("Enter password for testuser: ") cli.sendline("") - cli.expect_exact("Connection is not established") - cli.expect_exact("Connection refused") + cli.expect_exact("failed to connect to address 'localhost:0'") + cli.expect_exact("Connection refused (os error 111)") cli.expect_exact(pexpect.EOF) @@ -159,15 +135,7 @@ def test_connect_auth_type_ok(i1: Instance): cli.expect_exact("Enter password for testuser: ") cli.sendline("testpass") - cli.expect_exact(f"connected to {i1.host}:{i1.port}") - cli.expect_exact(f"{i1.host}:{i1.port}>") - - cli.sendline("\\set language lua") - cli.sendline("box.session.user()") - cli.expect_exact("---\r\n") - cli.expect_exact("- testuser\r\n") - cli.expect_exact("...\r\n") - cli.expect_exact("\r\n") + cli.expect_exact("picosql :)") eprint("^D") cli.sendcontrol("d") @@ -186,7 +154,7 @@ def test_connect_auth_type_different(i1: Instance): cli.expect_exact("Enter password for testuser: ") cli.sendline("") - cli.expect_exact("Connection is not established") + cli.expect_exact("service responded with error") cli.expect_exact("User not found or supplied credentials are invalid") cli.expect_exact(pexpect.EOF) @@ -205,10 +173,10 @@ def test_connect_auth_type_unknown(binary_path: str): cli.expect_exact(pexpect.EOF) -def test_connect_unix_enoent(binary_path: str): +def test_admin_enoent(binary_path: str): cli = pexpect.spawn( command=binary_path, - args=["connect", "--unix", "wrong/path/t.sock"], + args=["admin", "wrong/path/t.sock"], env={"NO_COLOR": "1"}, encoding="utf-8", timeout=1, @@ -221,10 +189,10 @@ def test_connect_unix_enoent(binary_path: str): cli.expect_exact(pexpect.EOF) -def test_connect_unix_econnrefused(binary_path: str): +def test_admin_econnrefused(binary_path: str): cli = pexpect.spawn( command=binary_path, - args=["connect", "--unix", "/dev/null"], + args=["admin", "/dev/null"], env={"NO_COLOR": "1"}, encoding="utf-8", timeout=1, @@ -237,10 +205,10 @@ def test_connect_unix_econnrefused(binary_path: str): cli.expect_exact(pexpect.EOF) -def test_connect_unix_invalid_path(binary_path: str): +def test_admin_invalid_path(binary_path: str): cli = pexpect.spawn( command=binary_path, - args=["connect", "--unix", "./[][]"], + args=["admin", "./[][]"], env={"NO_COLOR": "1"}, encoding="utf-8", timeout=1, @@ -251,10 +219,10 @@ def test_connect_unix_invalid_path(binary_path: str): cli.expect_exact(pexpect.EOF) -def test_connect_unix_empty_path(binary_path: str): +def test_admin_empty_path(binary_path: str): cli = pexpect.spawn( command=binary_path, - args=["connect", "--unix", ""], + args=["admin", ""], env={"NO_COLOR": "1"}, encoding="utf-8", timeout=1, @@ -265,7 +233,7 @@ def test_connect_unix_empty_path(binary_path: str): cli.expect_exact(pexpect.EOF) -def test_connect_unix_ok(cluster: Cluster): +def test_admin_ok(cluster: Cluster): i1 = cluster.add_instance(wait_online=False) i1.env.update({"PICODATA_CONSOLE_SOCK": f"{i1.data_dir}/console.sock"}) i1.start() @@ -281,7 +249,7 @@ def test_connect_unix_ok(cluster: Cluster): # We were unable to debug it quickly and used cwd as a workaround cwd=i1.data_dir, command=i1.binary_path, - args=["connect", "--unix", "./console.sock"], + args=["admin", "./console.sock"], encoding="utf-8", timeout=1, ) @@ -364,15 +332,7 @@ def test_connect_with_password_from_file(i1: Instance, binary_path: str): ) cli.logfile = sys.stdout - cli.expect_exact(f"connected to {i1.host}:{i1.port}") - cli.expect_exact(f"{i1.host}:{i1.port}>") - - cli.sendline("\\set language lua") - cli.sendline("box.session.user()") - cli.expect_exact("---\r\n") - cli.expect_exact("- testuser\r\n") - cli.expect_exact("...\r\n") - cli.expect_exact("\r\n") + cli.expect_exact("picosql :)") eprint("^D") cli.sendcontrol("d")