diff --git a/Cargo.lock b/Cargo.lock index ff1f7bb9900..f943336881e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2204,6 +2204,7 @@ dependencies = [ "directories", "dirs", "hex", + "itertools", "minicbor", "nix", "ockam", diff --git a/implementations/rust/ockam/ockam_api/src/cloud/enroll.rs b/implementations/rust/ockam/ockam_api/src/cloud/enroll.rs index c34df60fa66..c1fb8d0c442 100644 --- a/implementations/rust/ockam/ockam_api/src/cloud/enroll.rs +++ b/implementations/rust/ockam/ockam_api/src/cloud/enroll.rs @@ -3,10 +3,9 @@ use std::borrow::Cow; use minicbor::{Decode, Encode}; use serde::{Deserialize, Serialize}; -use ockam_core::{self, async_trait}; - #[cfg(feature = "tag")] -use ockam_core::api::TypeTag; +use ockam_core::TypeTag; +use ockam_core::{self, async_trait}; #[derive(Encode, Decode, Serialize, Deserialize, Debug)] #[cfg_attr(test, derive(PartialEq, Eq, Clone))] @@ -222,9 +221,10 @@ pub mod auth0 { } pub mod enrollment_token { - use crate::auth::types::Attributes; use serde::Serialize; + use crate::auth::types::Attributes; + use super::*; // Main req/res types diff --git a/implementations/rust/ockam/ockam_api/src/cloud/mod.rs b/implementations/rust/ockam/ockam_api/src/cloud/mod.rs index 39153eeffda..72e55604ceb 100644 --- a/implementations/rust/ockam/ockam_api/src/cloud/mod.rs +++ b/implementations/rust/ockam/ockam_api/src/cloud/mod.rs @@ -1,13 +1,14 @@ +use std::str::FromStr; + use minicbor::{Decode, Encode}; + +#[cfg(feature = "tag")] +use ockam_core::TypeTag; use ockam_core::{CowStr, Result, Route}; use ockam_multiaddr::MultiAddr; -use std::str::FromStr; use crate::error::ApiError; -#[cfg(feature = "tag")] -use crate::TypeTag; - pub mod enroll; pub mod project; pub mod space; diff --git a/implementations/rust/ockam/ockam_api/src/cloud/project.rs b/implementations/rust/ockam/ockam_api/src/cloud/project.rs index eb4e6976cf2..aeb4761f90a 100644 --- a/implementations/rust/ockam/ockam_api/src/cloud/project.rs +++ b/implementations/rust/ockam/ockam_api/src/cloud/project.rs @@ -1,9 +1,9 @@ use minicbor::{Decode, Encode}; -use ockam_core::CowStr; use serde::Serialize; +use ockam_core::CowStr; #[cfg(feature = "tag")] -use crate::TypeTag; +use ockam_core::TypeTag; #[derive(Encode, Decode, Serialize, Debug)] #[cfg_attr(test, derive(Clone))] @@ -346,11 +346,12 @@ mod tests { } mod node_api { + use ockam_core::api::Status; + use ockam_core::route; + use crate::cloud::CloudRequestWrapper; use crate::nodes::NodeManager; use crate::route_to_multiaddr; - use ockam_core::api::Status; - use ockam_core::route; use super::*; diff --git a/implementations/rust/ockam/ockam_api/src/cloud/space.rs b/implementations/rust/ockam/ockam_api/src/cloud/space.rs index a86abadc909..6d38ec52c16 100644 --- a/implementations/rust/ockam/ockam_api/src/cloud/space.rs +++ b/implementations/rust/ockam/ockam_api/src/cloud/space.rs @@ -2,9 +2,8 @@ use minicbor::{Decode, Encode}; use serde::Serialize; use ockam_core::CowStr; - #[cfg(feature = "tag")] -use crate::TypeTag; +use ockam_core::TypeTag; #[derive(Encode, Decode, Serialize, Debug)] #[cfg_attr(test, derive(Clone))] @@ -326,11 +325,12 @@ pub mod tests { } mod node_api { + use ockam_core::api::Status; + use ockam_core::route; + use crate::cloud::CloudRequestWrapper; use crate::nodes::NodeManager; use crate::route_to_multiaddr; - use ockam_core::api::Status; - use ockam_core::route; use super::*; diff --git a/implementations/rust/ockam/ockam_command/Cargo.toml b/implementations/rust/ockam/ockam_command/Cargo.toml index 6bfbdf471d2..3c8a6e30d38 100644 --- a/implementations/rust/ockam/ockam_command/Cargo.toml +++ b/implementations/rust/ockam/ockam_command/Cargo.toml @@ -56,6 +56,7 @@ dialoguer = "0.10" directories = "4" dirs = "4.0.0" hex = "0.4" +itertools = "0.10" minicbor = { version = "0.18.0", features = ["derive"] } nix = "0.24" open = "2" diff --git a/implementations/rust/ockam/ockam_command/src/authenticated.rs b/implementations/rust/ockam/ockam_command/src/authenticated.rs index 7583887e088..d7f53b724cb 100644 --- a/implementations/rust/ockam/ockam_command/src/authenticated.rs +++ b/implementations/rust/ockam/ockam_command/src/authenticated.rs @@ -43,7 +43,9 @@ pub enum AuthenticatedSubcommand { impl AuthenticatedCommand { pub fn run(c: AuthenticatedCommand) { - embedded_node(run_impl, c.subcommand) + if let Err(e) = embedded_node(run_impl, c.subcommand) { + eprintln!("Ockam node failed: {:?}", e,); + } } } diff --git a/implementations/rust/ockam/ockam_command/src/enroll/email.rs b/implementations/rust/ockam/ockam_command/src/enroll/email.rs index b15aedd4a71..805d628fab4 100644 --- a/implementations/rust/ockam/ockam_command/src/enroll/email.rs +++ b/implementations/rust/ockam/ockam_command/src/enroll/email.rs @@ -20,7 +20,9 @@ impl EnrollEmailCommand { println!("\nThank you for trying Ockam. We are working towards a developer release of Ockam Orchestrator in September. Please tell us your email and we'll let you know when we're ready to enroll new users to Ockam Orchestrator.\n"); let email = read_user_input().expect("couldn't read user input"); - embedded_node(enroll, (cmd, email)); + if let Err(e) = embedded_node(enroll, (cmd, email)) { + eprintln!("Ockam node failed: {:?}", e,); + } } } diff --git a/implementations/rust/ockam/ockam_command/src/error.rs b/implementations/rust/ockam/ockam_command/src/error.rs new file mode 100644 index 00000000000..a6a68a6ce14 --- /dev/null +++ b/implementations/rust/ockam/ockam_command/src/error.rs @@ -0,0 +1,32 @@ +use crate::util::ConfigError; +use crate::{exitcode, ExitCode}; +use tracing::error; + +pub type Result = std::result::Result; + +pub struct Error(ExitCode); + +impl Error { + pub fn new(code: ExitCode) -> Self { + assert!(code > 0, "Exit code can't be OK"); + Self(code) + } + + pub fn code(&self) -> ExitCode { + self.0 + } +} + +impl From for Error { + fn from(e: ConfigError) -> Self { + error!("{e}"); + Error::new(exitcode::CONFIG) + } +} + +impl From for Error { + fn from(e: anyhow::Error) -> Self { + error!("{e}"); + Error::new(exitcode::SOFTWARE) + } +} diff --git a/implementations/rust/ockam/ockam_command/src/lib.rs b/implementations/rust/ockam/ockam_command/src/lib.rs index c044579cdae..4360f08d678 100644 --- a/implementations/rust/ockam/ockam_command/src/lib.rs +++ b/implementations/rust/ockam/ockam_command/src/lib.rs @@ -32,6 +32,7 @@ use tcp::listener::TcpListenerCommand; use tcp::outlet::TcpOutletCommand; // to be removed +pub mod error; mod old; use old::cmd::identity::IdentityOpts; @@ -43,10 +44,13 @@ use old::{add_trusted, exit_with_result, node_subcommand, print_identity, print_ use crate::enroll::GenerateEnrollmentTokenCommand; use crate::identity::IdentityCommand; use crate::service::ServiceCommand; -use crate::util::OckamConfig; +use crate::util::exitcode::ExitCode; +use crate::util::{exitcode, stop_node, OckamConfig}; use crate::vault::VaultCommand; use clap::{crate_version, ArgEnum, Args, ColorChoice, Parser, Subcommand}; -use util::setup_logging; +use util::{embedded_node, setup_logging}; + +pub use error::{Error, Result}; const HELP_TEMPLATE: &str = "\ {before-help} diff --git a/implementations/rust/ockam/ockam_command/src/message/send.rs b/implementations/rust/ockam/ockam_command/src/message/send.rs index 92d892f92c3..e3e3bb5073f 100644 --- a/implementations/rust/ockam/ockam_command/src/message/send.rs +++ b/implementations/rust/ockam/ockam_command/src/message/send.rs @@ -3,7 +3,7 @@ use clap::Args; use minicbor::Decoder; use tracing::debug; -use crate::CommandGlobalOpts; +use crate::{embedded_node, CommandGlobalOpts}; use ockam::TcpTransport; use ockam_api::clean_multiaddr; use ockam_api::nodes::NODEMANAGER_ADDR; @@ -11,7 +11,7 @@ use ockam_core::api::{Response, Status}; use ockam_core::Route; use ockam_multiaddr::MultiAddr; -use crate::util::{api, connect_to, embedded_node, exitcode, get_final_element, stop_node}; +use crate::util::{api, connect_to, exitcode, stop_node}; #[derive(Clone, Debug, Args)] pub struct SendCommand { @@ -42,11 +42,10 @@ impl SendCommand { }; if let Some(node) = &cmd.from { - let name = get_final_element(node); - let port = opts.config.get_node_port(name); + let port = opts.config.get_node_port(node); connect_to(port, (opts, cmd), send_message_via_connection_to_a_node); - } else { - embedded_node(send_message_from_embedded_node, cmd) + } else if let Err(e) = embedded_node(send_message_from_embedded_node, cmd) { + eprintln!("Ockam node failed: {:?}", e,); } } } diff --git a/implementations/rust/ockam/ockam_command/src/node/create.rs b/implementations/rust/ockam/ockam_command/src/node/create.rs index 525f5e1476f..cf5123d1da7 100644 --- a/implementations/rust/ockam/ockam_command/src/node/create.rs +++ b/implementations/rust/ockam/ockam_command/src/node/create.rs @@ -140,7 +140,9 @@ impl CreateCommand { } } - embedded_node(setup, (command, cfg.clone())); + if let Err(e) = embedded_node(setup, (command, cfg.clone())) { + eprintln!("Ockam node failed: {:?}", e,); + } } else { // On systems with non-obvious path setups (or during // development) re-executing the current binary is a more diff --git a/implementations/rust/ockam/ockam_command/src/space/create.rs b/implementations/rust/ockam/ockam_command/src/space/create.rs index 9359a79985f..7b1f750dbf8 100644 --- a/implementations/rust/ockam/ockam_command/src/space/create.rs +++ b/implementations/rust/ockam/ockam_command/src/space/create.rs @@ -1,17 +1,12 @@ -use anyhow::{anyhow, Context}; use clap::Args; -use minicbor::Decoder; -use tracing::debug; +use ockam::Context; use ockam_api::cloud::space::Space; -use ockam_api::nodes::NODEMANAGER_ADDR; -use ockam_core::api::{Response, Status}; -use ockam_core::Route; use crate::node::NodeOpts; -use crate::util::api::CloudOpts; -use crate::util::{api, connect_to, exitcode, stop_node}; -use crate::{CommandGlobalOpts, OutputFormat}; +use crate::util::api::{self, CloudOpts}; +use crate::util::{node_rpc, Rpc}; +use crate::{stop_node, CommandGlobalOpts}; #[derive(Clone, Debug, Args)] pub struct CreateCommand { @@ -32,65 +27,25 @@ pub struct CreateCommand { impl CreateCommand { pub fn run(opts: CommandGlobalOpts, cmd: CreateCommand) { - let cfg = &opts.config; - let port = match cfg.select_node(&cmd.node_opts.api_node) { - Some(cfg) => cfg.port, - None => { - eprintln!("No such node available. Run `ockam node list` to list available nodes"); - std::process::exit(exitcode::IOERR); - } - }; - connect_to(port, (opts, cmd), create); + node_rpc(rpc, (opts, cmd)); } } -async fn create( - ctx: ockam::Context, +async fn rpc( + mut ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand), - mut base_route: Route, -) -> anyhow::Result<()> { - let route: Route = base_route.modify().append(NODEMANAGER_ADDR).into(); - debug!(?cmd, %route, "Sending request"); - - let response: Vec = ctx - .send_and_receive(route, api::space::create(cmd)?) - .await - .context("Failed to process request")?; - let mut dec = Decoder::new(&response); - let header = dec - .decode::() - .context("Failed to decode Response")?; - debug!(?header, "Received response"); - - let res = match (header.status(), header.has_body()) { - (Some(Status::Ok), true) => { - let body = dec - .decode::() - .context("Failed to decode response body")?; - let output = match opts.global_args.output_format { - OutputFormat::Plain => body.id.to_string(), - OutputFormat::Json => serde_json::to_string(&body) - .context("Failed to serialize command output as json")?, - }; - Ok(output) - } - (Some(status), true) => { - let err = dec - .decode::() - .unwrap_or_else(|_| "Unknown error".to_string()); - Err(anyhow!( - "An error occurred while processing the request with status code {status:?}: {err}" - )) - } - _ => Err(anyhow!("Unexpected response received from node")), - }; - match res { - Ok(o) => println!("{o}"), - Err(err) => { - eprintln!("{err}"); - std::process::exit(exitcode::IOERR); - } - }; +) -> crate::Result<()> { + let res = run_impl(&mut ctx, opts, cmd).await; + stop_node(ctx).await?; + res +} - stop_node(ctx).await +async fn run_impl( + ctx: &mut Context, + opts: CommandGlobalOpts, + cmd: CreateCommand, +) -> crate::Result<()> { + let mut rpc = Rpc::new(ctx, &opts, &cmd.node_opts.api_node)?; + rpc.request(api::space::create(&cmd)).await?; + rpc.print_response::() } diff --git a/implementations/rust/ockam/ockam_command/src/space/delete.rs b/implementations/rust/ockam/ockam_command/src/space/delete.rs index 84f4684858e..f830885c730 100644 --- a/implementations/rust/ockam/ockam_command/src/space/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/space/delete.rs @@ -1,17 +1,11 @@ -use anyhow::{anyhow, Context}; use clap::Args; -use minicbor::Decoder; -use serde_json::json; -use tracing::debug; -use ockam_api::nodes::NODEMANAGER_ADDR; -use ockam_core::api::{Response, Status}; -use ockam_core::Route; +use ockam::Context; use crate::node::NodeOpts; -use crate::util::api::CloudOpts; -use crate::util::{api, connect_to, exitcode, stop_node}; -use crate::{CommandGlobalOpts, OutputFormat}; +use crate::util::api::{self, CloudOpts}; +use crate::util::{node_rpc, Rpc}; +use crate::{stop_node, CommandGlobalOpts}; #[derive(Clone, Debug, Args)] pub struct DeleteCommand { @@ -28,63 +22,25 @@ pub struct DeleteCommand { impl DeleteCommand { pub fn run(opts: CommandGlobalOpts, cmd: DeleteCommand) { - let cfg = &opts.config; - let port = match cfg.select_node(&cmd.node_opts.api_node) { - Some(cfg) => cfg.port, - None => { - eprintln!("No such node available. Run `ockam node list` to list available nodes"); - std::process::exit(exitcode::IOERR); - } - }; - connect_to(port, (opts, cmd), delete); + node_rpc(rpc, (opts, cmd)); } } -async fn delete( - ctx: ockam::Context, +async fn rpc( + mut ctx: Context, (opts, cmd): (CommandGlobalOpts, DeleteCommand), - mut base_route: Route, -) -> anyhow::Result<()> { - let route: Route = base_route.modify().append(NODEMANAGER_ADDR).into(); - debug!(?cmd, %route, "Sending request"); - let space_id = cmd.id.clone(); - - let response: Vec = ctx - .send_and_receive(route, api::space::delete(cmd)?) - .await - .context("Failed to process request")?; - let mut dec = Decoder::new(&response); - let header = dec.decode::()?; - debug!(?header, "Received response"); - - let res = match header.status() { - Some(Status::Ok) => { - let output = match opts.global_args.output_format { - OutputFormat::Plain => "Space deleted".to_string(), - OutputFormat::Json => json!({ - "id": space_id, - }) - .to_string(), - }; - Ok(output) - } - Some(Status::InternalServerError) => { - let err = dec - .decode::() - .unwrap_or_else(|_| "Unknown error".to_string()); - Err(anyhow!( - "An error occurred while processing the request: {err}" - )) - } - _ => Err(anyhow!("Unexpected response received from node")), - }; - match res { - Ok(o) => println!("{o}"), - Err(err) => { - eprintln!("{err}"); - std::process::exit(exitcode::IOERR); - } - }; +) -> crate::Result<()> { + let res = run_impl(&mut ctx, opts, cmd).await; + stop_node(ctx).await?; + res +} - stop_node(ctx).await +async fn run_impl( + ctx: &mut Context, + opts: CommandGlobalOpts, + cmd: DeleteCommand, +) -> crate::Result<()> { + let mut rpc = Rpc::new(ctx, &opts, &cmd.node_opts.api_node)?; + rpc.request(api::space::delete(&cmd)).await?; + rpc.check_response() } diff --git a/implementations/rust/ockam/ockam_command/src/space/list.rs b/implementations/rust/ockam/ockam_command/src/space/list.rs index a82fbc1d34d..79ca320e4de 100644 --- a/implementations/rust/ockam/ockam_command/src/space/list.rs +++ b/implementations/rust/ockam/ockam_command/src/space/list.rs @@ -1,17 +1,12 @@ -use anyhow::{anyhow, Context}; use clap::Args; -use minicbor::Decoder; -use tracing::debug; +use ockam::Context; use ockam_api::cloud::space::Space; -use ockam_api::nodes::NODEMANAGER_ADDR; -use ockam_core::api::{Response, Status}; -use ockam_core::Route; use crate::node::NodeOpts; -use crate::util::api::CloudOpts; -use crate::util::{api, connect_to, exitcode, stop_node}; -use crate::{CommandGlobalOpts, OutputFormat}; +use crate::util::api::{self, CloudOpts}; +use crate::util::{node_rpc, Rpc}; +use crate::{stop_node, CommandGlobalOpts}; #[derive(Clone, Debug, Args)] pub struct ListCommand { @@ -24,60 +19,22 @@ pub struct ListCommand { impl ListCommand { pub fn run(opts: CommandGlobalOpts, cmd: ListCommand) { - let cfg = &opts.config; - let port = match cfg.select_node(&cmd.node_opts.api_node) { - Some(cfg) => cfg.port, - None => { - eprintln!("No such node available. Run `ockam node list` to list available nodes"); - std::process::exit(exitcode::IOERR); - } - }; - connect_to(port, (opts, cmd), list); + node_rpc(rpc, (opts, cmd)); } } -async fn list( - ctx: ockam::Context, - (opts, cmd): (CommandGlobalOpts, ListCommand), - mut base_route: Route, -) -> anyhow::Result<()> { - let route: Route = base_route.modify().append(NODEMANAGER_ADDR).into(); - debug!(?cmd, %route, "Sending request"); - - let response: Vec = ctx - .send_and_receive(route, api::space::list(cmd)?) - .await - .context("Failed to process request")?; - let mut dec = Decoder::new(&response); - let header = dec.decode::()?; - debug!(?header, "Received response"); - - let res = match header.status() { - Some(Status::Ok) => { - let body = dec.decode::>()?; - let output = match opts.global_args.output_format { - OutputFormat::Plain => format!("{body:#?}"), - OutputFormat::Json => serde_json::to_string(&body)?, - }; - Ok(output) - } - Some(Status::InternalServerError) => { - let err = dec - .decode::() - .unwrap_or_else(|_| "Unknown error".to_string()); - Err(anyhow!( - "An error occurred while processing the request: {err}" - )) - } - _ => Err(anyhow!("Unexpected response received from node")), - }; - match res { - Ok(o) => println!("{o}"), - Err(err) => { - eprintln!("{err}"); - std::process::exit(exitcode::IOERR); - } - }; +async fn rpc(mut ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand)) -> crate::Result<()> { + let res = run_impl(&mut ctx, opts, cmd).await; + stop_node(ctx).await?; + res +} - stop_node(ctx).await +async fn run_impl( + ctx: &mut Context, + opts: CommandGlobalOpts, + cmd: ListCommand, +) -> crate::Result<()> { + let mut rpc = Rpc::new(ctx, &opts, &cmd.node_opts.api_node)?; + rpc.request(api::space::list(&cmd)).await?; + rpc.print_response::>() } diff --git a/implementations/rust/ockam/ockam_command/src/space/show.rs b/implementations/rust/ockam/ockam_command/src/space/show.rs index acd56a3f397..236260f8e29 100644 --- a/implementations/rust/ockam/ockam_command/src/space/show.rs +++ b/implementations/rust/ockam/ockam_command/src/space/show.rs @@ -1,17 +1,12 @@ -use anyhow::{anyhow, Context}; use clap::Args; -use minicbor::Decoder; -use tracing::debug; +use ockam::Context; use ockam_api::cloud::space::Space; -use ockam_api::nodes::NODEMANAGER_ADDR; -use ockam_core::api::{Response, Status}; -use ockam_core::Route; use crate::node::NodeOpts; -use crate::util::api::CloudOpts; -use crate::util::{api, connect_to, exitcode, stop_node}; -use crate::{CommandGlobalOpts, OutputFormat}; +use crate::util::api::{self, CloudOpts}; +use crate::util::{node_rpc, Rpc}; +use crate::{stop_node, CommandGlobalOpts}; #[derive(Clone, Debug, Args)] pub struct ShowCommand { @@ -28,60 +23,22 @@ pub struct ShowCommand { impl ShowCommand { pub fn run(opts: CommandGlobalOpts, cmd: ShowCommand) { - let cfg = &opts.config; - let port = match cfg.select_node(&cmd.node_opts.api_node) { - Some(cfg) => cfg.port, - None => { - eprintln!("No such node available. Run `ockam node list` to list available nodes"); - std::process::exit(exitcode::IOERR); - } - }; - connect_to(port, (opts, cmd), show); + node_rpc(rpc, (opts, cmd)); } } -async fn show( - ctx: ockam::Context, - (opts, cmd): (CommandGlobalOpts, ShowCommand), - mut base_route: Route, -) -> anyhow::Result<()> { - let route: Route = base_route.modify().append(NODEMANAGER_ADDR).into(); - debug!(?cmd, %route, "Sending request"); - - let response: Vec = ctx - .send_and_receive(route, api::space::show(cmd)?) - .await - .context("Failed to process request")?; - let mut dec = Decoder::new(&response); - let header = dec.decode::()?; - debug!(?header, "Received response"); - - let res = match header.status() { - Some(Status::Ok) => { - let body = dec.decode::()?; - let output = match opts.global_args.output_format { - OutputFormat::Plain => format!("{body:#?}"), - OutputFormat::Json => serde_json::to_string(&body)?, - }; - Ok(output) - } - Some(Status::InternalServerError) => { - let err = dec - .decode::() - .unwrap_or_else(|_| "Unknown error".to_string()); - Err(anyhow!( - "An error occurred while processing the request: {err}" - )) - } - _ => Err(anyhow!("Unexpected response received from node")), - }; - match res { - Ok(o) => println!("{o}"), - Err(err) => { - eprintln!("{err}"); - std::process::exit(exitcode::IOERR); - } - }; +async fn rpc(mut ctx: Context, (opts, cmd): (CommandGlobalOpts, ShowCommand)) -> crate::Result<()> { + let res = run_impl(&mut ctx, opts, cmd).await; + stop_node(ctx).await?; + res +} - stop_node(ctx).await +async fn run_impl( + ctx: &mut Context, + opts: CommandGlobalOpts, + cmd: ShowCommand, +) -> crate::Result<()> { + let mut rpc = Rpc::new(ctx, &opts, &cmd.node_opts.api_node)?; + rpc.request(api::space::show(&cmd)).await?; + rpc.print_response::() } diff --git a/implementations/rust/ockam/ockam_command/src/util/api.rs b/implementations/rust/ockam/ockam_command/src/util/api.rs index 5602709aa2d..f7eb65f638b 100644 --- a/implementations/rust/ockam/ockam_command/src/util/api.rs +++ b/implementations/rust/ockam/ockam_command/src/util/api.rs @@ -247,41 +247,30 @@ pub(crate) mod enroll { /// Helpers to create spaces API requests pub(crate) mod space { use crate::space::*; - use ockam_api::cloud::space::*; + use ockam_api::cloud::{space::*, BareCloudRequestWrapper}; + use ockam_core::api::RequestBuilder; use super::*; - pub(crate) fn create(cmd: CreateCommand) -> anyhow::Result> { + pub(crate) fn create(cmd: &CreateCommand) -> RequestBuilder> { let b = CreateSpace::new(cmd.name.as_str(), &cmd.admins); - let mut buf = vec![]; Request::builder(Method::Post, "v0/spaces") .body(CloudRequestWrapper::new(b, cmd.cloud_opts.route())) - .encode(&mut buf)?; - Ok(buf) } - pub(crate) fn list(cmd: ListCommand) -> anyhow::Result> { - let mut buf = vec![]; + pub(crate) fn list(cmd: &ListCommand) -> RequestBuilder { Request::builder(Method::Get, "v0/spaces") .body(CloudRequestWrapper::bare(cmd.cloud_opts.route())) - .encode(&mut buf)?; - Ok(buf) } - pub(crate) fn show(cmd: ShowCommand) -> anyhow::Result> { - let mut buf = vec![]; + pub(crate) fn show(cmd: &ShowCommand) -> RequestBuilder { Request::builder(Method::Get, format!("v0/spaces/{}", cmd.id)) .body(CloudRequestWrapper::bare(cmd.cloud_opts.route())) - .encode(&mut buf)?; - Ok(buf) } - pub(crate) fn delete(cmd: DeleteCommand) -> anyhow::Result> { - let mut buf = vec![]; + pub(crate) fn delete(cmd: &DeleteCommand) -> RequestBuilder { Request::builder(Method::Delete, format!("v0/spaces/{}", cmd.id)) .body(CloudRequestWrapper::bare(cmd.cloud_opts.route())) - .encode(&mut buf)?; - Ok(buf) } } diff --git a/implementations/rust/ockam/ockam_command/src/util/mod.rs b/implementations/rust/ockam/ockam_command/src/util/mod.rs index bf9a72f78fc..8f10428af59 100644 --- a/implementations/rust/ockam/ockam_command/src/util/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/util/mod.rs @@ -1,22 +1,125 @@ +use std::{env, net::TcpListener, path::Path}; + +use anyhow::Context; +use minicbor::{Decode, Decoder, Encode}; +use tracing::error; +use tracing_subscriber::prelude::*; +use tracing_subscriber::{filter::LevelFilter, fmt, EnvFilter}; + +pub use addon::AddonCommand; +pub use config::*; +use ockam::{route, Address, NodeBuilder, Route, TcpTransport, TCP}; +use ockam_api::nodes::NODEMANAGER_ADDR; +use ockam_core::api::{RequestBuilder, Response, Status}; + +use crate::util::output::Output; +use crate::{CommandGlobalOpts, OutputFormat}; + pub mod api; pub mod exitcode; pub mod startup; mod addon; -pub use addon::AddonCommand; - mod config; -pub use config::*; - -use anyhow::Context; -use ockam::{route, NodeBuilder, Route, TcpTransport, TCP}; -use std::{env, net::TcpListener, path::Path}; -use tracing::error; -use tracing_subscriber::prelude::*; -use tracing_subscriber::{filter::LevelFilter, fmt, EnvFilter}; +mod output; pub const DEFAULT_CLOUD_ADDRESS: &str = "/dnsaddr/cloud.ockam.io/tcp/62526"; +pub struct Rpc<'a> { + ctx: &'a ockam::Context, + buf: Vec, + opts: &'a CommandGlobalOpts, + cfg: NodeConfig, +} + +impl<'a> Rpc<'a> { + pub fn new( + ctx: &'a ockam::Context, + opts: &'a CommandGlobalOpts, + api_node: &'a str, + ) -> anyhow::Result { + Ok(Rpc { + ctx, + buf: Vec::new(), + opts, + cfg: opts.config.get_node(api_node)?, + }) + } + + pub async fn request(&mut self, req: RequestBuilder<'_, T>) -> anyhow::Result<()> + where + T: Encode<()>, + { + let buf = req.to_vec()?; + let mut rte = connect(self.ctx, &self.cfg).await?; + self.buf = self + .ctx + .send_and_receive(rte.modify().append(NODEMANAGER_ADDR), buf) + .await?; + Ok(()) + } + + /// Parse the response body and return it. + pub fn parse_response(&'a self) -> crate::Result + where + T: Decode<'a, ()>, + { + let mut dec = self.parse_response_impl()?; + Ok(dec.decode().context("Failed to decode response body")?) + } + + /// Parse response header only to check the status code. + pub fn check_response(&self) -> crate::Result<()> { + self.parse_response_impl()?; + Ok(()) + } + + /// Parse the response body and return it. + fn parse_response_impl(&'a self) -> crate::Result { + let mut dec = Decoder::new(&self.buf); + let hdr = dec + .decode::() + .context("Failed to decode response header")?; + match hdr.status() { + Some(Status::Ok) if hdr.has_body() => { + return Ok(dec); + } + Some(Status::Ok) => { + eprintln!("No body found in response"); + } + Some(status) if hdr.has_body() => { + let err = dec.decode::().unwrap_or_default(); + eprintln!( + "An error occurred while processing the request. Status code: {status}. {err}" + ); + } + Some(status) => { + eprintln!("An error occurred while processing the request. Status code: {status}",); + } + None => { + eprintln!("No status found in response"); + } + }; + Err(crate::Error::new(exitcode::SOFTWARE)) + } + + /// Parse the response body and print it. + pub fn print_response(&'a self) -> crate::Result<()> + where + T: Decode<'a, ()> + Output + serde::Serialize, + { + let b: T = self.parse_response()?; + let o = match self.opts.global_args.output_format { + OutputFormat::Plain => b.output().context("Failed to serialize response body")?, + OutputFormat::Json => { + serde_json::to_string_pretty(&b).context("Failed to serialize response body")? + } + }; + println!("{}", o); + Ok(()) + } +} + /// A simple wrapper for shutting down the local embedded node (for /// the client side of the CLI). Swallows errors and turns them into /// eprintln logs. @@ -25,7 +128,6 @@ pub const DEFAULT_CLOUD_ADDRESS: &str = "/dnsaddr/cloud.ockam.io/tcp/62526"; pub async fn stop_node(mut ctx: ockam::Context) -> anyhow::Result<()> { if let Err(e) = ctx.stop().await { eprintln!("an error occurred while shutting down local node: {}", e); - std::process::exit(exitcode::IOERR); } Ok(()) } @@ -46,7 +148,7 @@ where F: FnOnce(ockam::Context, A, Route) -> Fut + Send + Sync + 'static, Fut: core::future::Future> + Send + 'static, { - embedded_node( + let res = embedded_node( move |ctx, a| async move { let tcp = match TcpTransport::create(&ctx).await { Ok(tcp) => tcp, @@ -70,28 +172,62 @@ where Ok(()) }, a, - ) + ); + if let Err(e) = res { + eprintln!("Ockam node failed: {:?}", e,); + } +} + +async fn connect(ctx: &ockam::Context, cfg: &NodeConfig) -> anyhow::Result { + let adr = Address::from((TCP, format!("localhost:{}", cfg.port))); + let tcp = TcpTransport::create(ctx).await?; + tcp.connect(adr.address()).await?; + Ok(adr.into()) } -pub fn embedded_node(f: F, a: A) +pub fn node_rpc(f: F, a: A) where A: Send + Sync + 'static, F: FnOnce(ockam::Context, A) -> Fut + Send + Sync + 'static, - Fut: core::future::Future> + Send + 'static, + Fut: core::future::Future> + Send + 'static, { - let (ctx, mut executor) = NodeBuilder::without_access_control().no_logging().build(); - let res = executor.execute(async move { - if let Err(e) = f(ctx, a).await { - eprintln!("Error {:?}", e); - std::process::exit(exitcode::IOERR); - } - }); + let res = embedded_node( + |ctx, a| async { + let res = f(ctx, a).await; + if let Err(e) = res { + std::process::exit(e.code()); + } + Ok(()) + }, + a, + ); if let Err(e) = res { - eprintln!("Ockam node failed: {:?}", e,); - std::process::exit(exitcode::IOERR); + eprintln!("Ockam node failed: {:?}", e); + std::process::exit(exitcode::SOFTWARE); } } +pub fn embedded_node(f: F, a: A) -> anyhow::Result +where + A: Send + Sync + 'static, + F: FnOnce(ockam::Context, A) -> Fut + Send + Sync + 'static, + Fut: core::future::Future> + Send + 'static, + T: Send + 'static, +{ + let (ctx, mut executor) = NodeBuilder::without_access_control().no_logging().build(); + executor + .execute(async move { + match f(ctx, a).await { + Err(e) => { + eprintln!("Error {:?}", e); + std::process::exit(1); + } + Ok(v) => v, + } + }) + .map_err(anyhow::Error::from) +} + pub fn find_available_port() -> anyhow::Result { let listener = TcpListener::bind("127.0.0.1:0").context("Unable to bind to an open port")?; let address = listener @@ -138,8 +274,7 @@ pub fn setup_logging(verbose: u8, no_color: bool) { .with(fmt) .try_init(); if result.is_err() { - eprintln!("Failed to initialize tracing logging."); - std::process::exit(exitcode::IOERR); + eprintln!("Failed to initialise tracing logging."); } } @@ -166,3 +301,10 @@ pub fn get_final_element(input_path: &str) -> &str { input_path }; } + +pub fn comma_separated>(data: &[T]) -> String { + use itertools::Itertools; + + #[allow(unstable_name_collisions)] + data.iter().map(AsRef::as_ref).intersperse(", ").collect() +} diff --git a/implementations/rust/ockam/ockam_command/src/util/output.rs b/implementations/rust/ockam/ockam_command/src/util/output.rs new file mode 100644 index 00000000000..f2d86d2458d --- /dev/null +++ b/implementations/rust/ockam/ockam_command/src/util/output.rs @@ -0,0 +1,65 @@ +use cli_table::{Cell, Style, Table}; +use core::fmt::Write; + +use crate::util::comma_separated; +use ockam_api::cloud::space::Space; + +/// Trait to control how a given type will be printed as a CLI output. +/// +/// The `Output` allows us to reuse the same formatting logic across different commands +/// and extract the formatting logic out of the commands logic. +/// +/// Note that we can't just implement the `Display` trait because most of the types we want +/// to output in the commands are defined in other crates. We can still reuse the `Display` +/// implementation if it's available and already formats the type as we want. For example: +/// +/// ```ignore +/// struct MyType; +/// +/// impl std::fmt::Display for MyType { +/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +/// write!(f, "MyType") +/// } +/// } +/// +/// impl Output for MyType { +/// fn output(&self) -> anyhow::Result { +/// Ok(self.to_string()) +/// } +/// } +/// ``` +pub trait Output { + fn output(&self) -> anyhow::Result; +} + +impl Output for Space<'_> { + fn output(&self) -> anyhow::Result { + let mut w = String::new(); + write!(w, "id: {}", self.id)?; + write!(w, "\nname: {}", self.name)?; + write!(w, "\nusers: {}", comma_separated(&self.users))?; + Ok(w) + } +} + +impl Output for Vec> { + fn output(&self) -> anyhow::Result { + let mut rows = vec![]; + for Space { + id, name, users, .. + } in self + { + rows.push([id.cell(), name.cell(), comma_separated(users).cell()]); + } + let table = rows + .table() + .title([ + "ID".cell().bold(true), + "Name".cell().bold(true), + "Users".cell().bold(true), + ]) + .display()? + .to_string(); + Ok(table) + } +} diff --git a/implementations/rust/ockam/ockam_core/src/api.rs b/implementations/rust/ockam/ockam_core/src/api.rs index 502889bc2fa..e1b13a3b254 100644 --- a/implementations/rust/ockam/ockam_core/src/api.rs +++ b/implementations/rust/ockam/ockam_core/src/api.rs @@ -150,6 +150,22 @@ pub enum Status { #[n(501)] NotImplemented } +impl Display for Status { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str(match self { + Status::Ok => "200 Ok", + Status::BadRequest => "400 BadRequest", + Status::Unauthorized => "401 Unauthorized", + Status::Forbidden => "403 Forbidden", + Status::NotFound => "404 NotFound", + Status::Conflict => "409 Conflict", + Status::MethodNotAllowed => "405 MethodNotAllowed", + Status::InternalServerError => "500 InternalServerError", + Status::NotImplemented => "501 NotImplemented", + }) + } +} + impl Id { pub fn fresh() -> Self { Id(rand::random()) diff --git a/implementations/rust/ockam/ockam_core/src/cbor_utils/cow_str.rs b/implementations/rust/ockam/ockam_core/src/cbor_utils/cow_str.rs index 703c74bd869..3b28b34cd61 100644 --- a/implementations/rust/ockam/ockam_core/src/cbor_utils/cow_str.rs +++ b/implementations/rust/ockam/ockam_core/src/cbor_utils/cow_str.rs @@ -12,9 +12,7 @@ use serde::{Deserialize, Serialize}; /// Contrary to `Cow<_, str>` the `Decode` impl for this type will always borrow /// from input so using it in types like `Option`, `Vec<_>` etc will not produce /// owned element values. -#[derive( - Debug, Clone, Encode, Decode, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash, -)] +#[derive(Debug, Clone, Encode, Decode, Serialize, Deserialize, Eq, PartialOrd, Ord, Hash)] #[cbor(transparent)] #[serde(transparent)] pub struct CowStr<'a>( @@ -77,3 +75,9 @@ impl<'a, S: ?Sized + AsRef> PartialEq for CowStr<'a> { self.0 == other.as_ref() } } + +impl<'a> AsRef for CowStr<'a> { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +}