From 179ae09c949e23f19f11b206fa9d52fdc0902bb8 Mon Sep 17 00:00:00 2001 From: Anas Elgarhy Date: Sun, 15 Oct 2023 19:30:46 +0300 Subject: [PATCH] feat(rust): show interactive menu to select one or more TCP inlent when user dosen't specifies Make ockam tcp-inlet delete (no args) interactive by asking the user to choose from a list of `tcp-inlet` aliases to delete using tuify - Closes: build-trust/ockam/issues/6466 --- .../src/nodes/service/background_node.rs | 5 + .../rust/ockam/ockam_command/Cargo.toml | 4 +- .../ockam_command/src/tcp/inlet/delete.rs | 96 +++++++++++++------ .../rust/ockam/ockam_command/src/tcp/util.rs | 36 +++++++ 4 files changed, 109 insertions(+), 32 deletions(-) diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/background_node.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/background_node.rs index 3c3850a5949..872aa2afb39 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/background_node.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/background_node.rs @@ -158,4 +158,9 @@ impl BackgroundNode { let route = self.create_route().await?; Ok(Client::new(&route, timeout)) } + + /// Get the node name + pub fn name(&self) -> &str { + &self.node_name + } } diff --git a/implementations/rust/ockam/ockam_command/Cargo.toml b/implementations/rust/ockam/ockam_command/Cargo.toml index 1d4a5f7c8b6..4d4012cbd6b 100644 --- a/implementations/rust/ockam/ockam_command/Cargo.toml +++ b/implementations/rust/ockam/ockam_command/Cargo.toml @@ -89,13 +89,13 @@ ockam_vault_aws = { path = "../ockam_vault_aws", version = "^0.12.0" } once_cell = "1.18" open = "5.0.0" pem-rfc7468 = { version = "0.7.0", features = ["std"] } -r3bl_rs_utils_core = "0.9.7" -r3bl_tuify = "0.1.19" rand = "0.8" regex = "1.10.2" reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls-native-roots"] } rustls = "0.21.7" rustls-native-certs = "0.6.3" +r3bl_tuify = "0.1.17" +r3bl_rs_utils_core = "0.9.5" serde = { version = "1", features = ["derive"] } serde_bare = { version = "0.5.0", default-features = false, features = ["alloc"] } serde_json = "1" diff --git a/implementations/rust/ockam/ockam_command/src/tcp/inlet/delete.rs b/implementations/rust/ockam/ockam_command/src/tcp/inlet/delete.rs index 8926af9755a..005486df7b4 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/inlet/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/inlet/delete.rs @@ -1,17 +1,18 @@ use clap::Args; use colorful::Colorful; +use indoc::formatdoc; use miette::{miette, IntoDiagnostic}; use ockam::Context; -use ockam_api::nodes::models::portal::InletStatus; +use ockam_api::nodes::models::portal::{InletList, InletStatus}; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; -use crate::fmt_ok; use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; -use crate::tcp::util::alias_parser; +use crate::tcp::util::{alias_parser, fetch_list}; use crate::util::{node_rpc, parse_node_name}; use crate::{docs, CommandGlobalOpts}; +use crate::{fmt_err, fmt_ok}; const AFTER_LONG_HELP: &str = include_str!("./static/delete/after_long_help.txt"); @@ -20,8 +21,8 @@ const AFTER_LONG_HELP: &str = include_str!("./static/delete/after_long_help.txt" #[command(after_long_help = docs::after_help(AFTER_LONG_HELP))] pub struct DeleteCommand { /// Delete the inlet with this alias - #[arg(display_order = 900, required = true, id = "ALIAS", value_parser = alias_parser)] - alias: String, + #[arg(display_order = 900, id = "ALIAS", value_parser = alias_parser)] + alias: Option, /// Node on which to stop the tcp inlet. If none are provided, the default node will be used #[command(flatten)] @@ -43,37 +44,72 @@ pub async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, DeleteCommand), ) -> miette::Result<()> { + let mut selected_aliases = Vec::with_capacity(1); let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); let node_name = parse_node_name(&node_name)?; let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; - // Check if alias exists - let alias = cmd.alias; - node.ask_and_get_reply::<_, InletStatus>(&ctx, Request::get(format!("/node/inlet/{alias}"))) - .await? - .found() - .into_diagnostic()? - .ok_or(miette!( - "TCP inlet with alias {alias} was not found on Node {node_name}" - ))?; + if let Some(alias) = cmd.alias.as_ref() { + // Check if alias exists + let inlet_staus = node + .ask_and_get_reply::<_, InletStatus>(&ctx, Request::get(format!("/node/inlet/{alias}"))) + .await? + .found() + .into_diagnostic()? + .ok_or(miette!( + "TCP inlet with alias {alias} was not found on Node {node_name}" + ))?; + selected_aliases.push(inlet_staus.alias); + } else { + if !opts.terminal.can_ask_for_user_input() { + return Err(miette!("No alias was provided")); + } + // Get all the avilable aliases on the node + let aliases: Vec = + fetch_list::<_, InletList>("/node/inlet", &ctx, &node, &opts.terminal) + .await? + .list + .into_iter() + .map(|status| status.alias) + .collect(); + if aliases.is_empty() { + return Err(miette!("There's no TCP inlents to choose from")); + } + // Show the multible selection menu + selected_aliases = opts + .terminal + .select_multiple("Select an inlet/s to delete".to_string(), aliases); + if selected_aliases.is_empty() { + return Err(miette!("User did not select anything")); + }; + } - // Proceed with the deletion - if opts - .terminal - .confirmed_with_flag_or_prompt(cmd.yes, "Are you sure you want to delete this TCP inlet?")? - { - node.tell(&ctx, Request::delete(format!("/node/inlet/{alias}"))) - .await?; + // Confirm that the user know what he do + let know = if cmd.alias.is_none() { + opts.terminal.confirm_interactively(formatdoc!( + "Are you sure that you like to delete these items : {:?}?", + selected_aliases + )) + } else { + opts.terminal.confirmed_with_flag_or_prompt( + cmd.yes, + "Are you sure that you want to delete this TCP inlet?", + )? + }; - opts.terminal - .stdout() - .plain(fmt_ok!( - "TCP inlet with alias {alias} on Node {node_name} has been deleted" - )) - .machine(&alias) - .json(serde_json::json!({ "alias": alias, "node": node_name })) - .write_line() - .unwrap(); + if know { + for alias in selected_aliases { + let msg = match node + .tell(&ctx, Request::delete(format!("/node/inlet/{alias}"))) + .await + { + Ok(_) => fmt_ok!( + "✅ TCP inlet with alias `{alias}` on Node {node_name} has been deleted" + ), + Err(e) => fmt_err!("⚠️ Can't delete `{alias}` becuse: {e}"), + }; + opts.terminal.clone().stdout().plain(msg).write_line()?; + } } Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/tcp/util.rs b/implementations/rust/ockam/ockam_command/src/tcp/util.rs index f72d0e1f7f9..d672a346367 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/util.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/util.rs @@ -1,5 +1,17 @@ +use crate::terminal::{OckamColor, Terminal, TerminalWriter}; use crate::Result; +use colorful::Colorful; use miette::miette; +use ockam::Context; +use ockam_api::nodes::BackgroundNode; +use ockam_core::api::Request; + + + + + +use tokio::sync::Mutex; +use tokio::try_join; pub fn alias_parser(arg: &str) -> Result { if arg.contains(':') { @@ -8,3 +20,27 @@ pub fn alias_parser(arg: &str) -> Result { Ok(arg.to_string()) } } + +pub async fn fetch_list minicbor::Decode<'b, ()>>( + endpoint: &str, + ctx: &Context, + node: &BackgroundNode, + terminal: &Terminal, +) -> miette::Result { + let is_finished: Mutex = Mutex::new(false); + + let get_list = async { + let items: L = node.ask(ctx, Request::get(endpoint)).await?; + *is_finished.lock().await = true; + Ok(items) + }; + let output_messages = vec![format!( + "Listing TCP Inlets on {}...\n", + node.name().color(OckamColor::PrimaryResource.color()) + )]; + + let progress_output = terminal.progress_output(&output_messages, &is_finished); + + let (items, _) = try_join!(get_list, progress_output)?; + Ok(items) +}