From ed985f6fe0cdc1a204e28ff3890a8f7963a32921 Mon Sep 17 00:00:00 2001 From: Fintan Halpenny Date: Wed, 22 Feb 2023 13:00:16 +0000 Subject: [PATCH] cli: rad node This adds a rad node subcommand to the CLI. The subcommand aims to fulfill the following requirements: - Checking the node status (rad node status) - Connecting to the node's control socket (rad node connect) - Printing out our routing table (rad node routing) - Printing out our tracking policies (rad node tracking) The following are yet to be implemented: - Starting the node (rad node start) - Stopping the node (rad node stop) Signed-off-by: Fintan Halpenny X-Clacks-Overhead: GNU Terry Pratchett --- radicle-cli/src/commands.rs | 2 + radicle-cli/src/commands/help.rs | 1 + radicle-cli/src/commands/node.rs | 161 ++++++++++++++++++++++ radicle-cli/src/commands/node/control.rs | 44 ++++++ radicle-cli/src/commands/node/routing.rs | 17 +++ radicle-cli/src/commands/node/tracking.rs | 49 +++++++ radicle-cli/src/main.rs | 8 ++ radicle-cli/src/terminal/args.rs | 6 + 8 files changed, 288 insertions(+) create mode 100644 radicle-cli/src/commands/node.rs create mode 100644 radicle-cli/src/commands/node/control.rs create mode 100644 radicle-cli/src/commands/node/routing.rs create mode 100644 radicle-cli/src/commands/node/tracking.rs diff --git a/radicle-cli/src/commands.rs b/radicle-cli/src/commands.rs index e6eeb616..ef2d6898 100644 --- a/radicle-cli/src/commands.rs +++ b/radicle-cli/src/commands.rs @@ -28,6 +28,8 @@ pub mod rad_issue; pub mod rad_ls; #[path = "commands/merge.rs"] pub mod rad_merge; +#[path = "commands/node.rs"] +pub mod rad_node; #[path = "commands/patch.rs"] pub mod rad_patch; #[path = "commands/path.rs"] diff --git a/radicle-cli/src/commands/help.rs b/radicle-cli/src/commands/help.rs index a1d3993e..bc80e601 100644 --- a/radicle-cli/src/commands/help.rs +++ b/radicle-cli/src/commands/help.rs @@ -26,6 +26,7 @@ const COMMANDS: &[Help] = &[ rad_issue::HELP, rad_ls::HELP, rad_merge::HELP, + rad_node::HELP, rad_patch::HELP, rad_path::HELP, rad_push::HELP, diff --git a/radicle-cli/src/commands/node.rs b/radicle-cli/src/commands/node.rs new file mode 100644 index 00000000..caa49791 --- /dev/null +++ b/radicle-cli/src/commands/node.rs @@ -0,0 +1,161 @@ +use std::ffi::OsString; + +use anyhow::anyhow; +use radicle::node::{Address, Node, NodeId}; + +use crate::terminal as term; +use crate::terminal::args::{Args, Error, Help}; + +#[path = "node/control.rs"] +mod control; +#[path = "node/routing.rs"] +mod routing; +#[path = "node/tracking.rs"] +mod tracking; + +pub const HELP: Help = Help { + name: "node", + description: "Control and query the Radicle Node", + version: env!("CARGO_PKG_VERSION"), + usage: r#" +Usage + + rad node status + rad node (start|stop) + rad node connect + rad node routing + rad node tracking [--repos|--nodes] + +Options + + --help Print help + --repos Show the tracked repositories table + --nodes Show the tracked nodes table +"#, +}; + +pub struct Options { + op: Operation, +} + +pub enum Operation { + Connect { nid: NodeId, addr: Address }, + Routing, + Start, + Status, + Stop, + Tracking { mode: TrackingMode }, +} + +#[derive(Default)] +pub enum TrackingMode { + #[default] + Repos, + Nodes, +} + +#[derive(Default)] +pub enum OperationName { + Connect, + Routing, + Start, + #[default] + Status, + Stop, + Tracking, +} + +impl Args for Options { + fn from_args(args: Vec) -> anyhow::Result<(Self, Vec)> { + use lexopt::prelude::*; + + let mut parser = lexopt::Parser::from_args(args); + let mut op: Option = None; + let mut tracking_mode = TrackingMode::default(); + let mut nid: Option = None; + let mut addr: Option
= None; + + while let Some(arg) = parser.next()? { + match arg { + Long("help") => { + return Err(Error::Help.into()); + } + Value(val) if op.is_none() => match val.to_string_lossy().as_ref() { + "connect" => op = Some(OperationName::Connect), + "routing" => op = Some(OperationName::Routing), + "start" => op = Some(OperationName::Start), + "status" => op = Some(OperationName::Status), + "stop" => op = Some(OperationName::Stop), + "tracking" => op = Some(OperationName::Tracking), + + unknown => anyhow::bail!("unknown operation '{}'", unknown), + }, + Value(val) if matches!(op, Some(OperationName::Connect)) => { + match term::args::nid(&val) { + Ok(val) => { + nid = Some(val); + } + Err(e1) => match term::args::addr(&val) { + Ok(val) => { + addr = Some(val); + } + Err(e2) => return Err(anyhow!("'{}' or '{}'", e1, e2)), + }, + } + } + Long("repos") if matches!(op, Some(OperationName::Tracking)) => { + tracking_mode = TrackingMode::Repos + } + Long("nodes") if matches!(op, Some(OperationName::Tracking)) => { + tracking_mode = TrackingMode::Nodes + } + _ => return Err(anyhow!(arg.unexpected())), + } + } + + let op = match op.unwrap_or_default() { + OperationName::Connect => Operation::Connect { + nid: nid.ok_or_else(|| anyhow!("a NID must be provided"))?, + addr: addr.ok_or_else(|| anyhow!("an address must be provided"))?, + }, + OperationName::Routing => Operation::Routing, + OperationName::Start => Operation::Start, + OperationName::Status => Operation::Status, + OperationName::Stop => Operation::Stop, + OperationName::Tracking => Operation::Tracking { + mode: tracking_mode, + }, + }; + Ok((Options { op }, vec![])) + } +} + +pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { + let profile = ctx.profile()?; + + match options.op { + Operation::Connect { nid, addr } => { + let mut node = Node::new(profile.socket()); + control::connect(&mut node, nid, addr)? + } + Operation::Routing => { + let node = Node::new(profile.socket()); + routing::run(&node)?; + } + Operation::Start => control::start()?, + Operation::Status => { + let node = Node::new(profile.socket()); + control::status(&node); + } + Operation::Stop => { + let node = Node::new(profile.socket()); + control::stop(node)?; + } + Operation::Tracking { mode } => { + let node = Node::new(profile.socket()); + tracking::run(&node, mode)? + } + } + + Ok(()) +} diff --git a/radicle-cli/src/commands/node/control.rs b/radicle-cli/src/commands/node/control.rs new file mode 100644 index 00000000..a794dfcc --- /dev/null +++ b/radicle-cli/src/commands/node/control.rs @@ -0,0 +1,44 @@ +use radicle::node::{Address, Handle as _, NodeId}; +use radicle::Node; + +use crate::terminal as term; + +pub fn start() -> anyhow::Result<()> { + todo!() +} + +pub fn stop(node: Node) -> anyhow::Result<()> { + let spinner = term::spinner("Stopping the node..."); + if let Err(err) = node.shutdown() { + spinner.error(format!("Error occurred while shutting down node: {err}")); + } else { + spinner.finish(); + } + Ok(()) +} + +pub fn connect(node: &mut Node, nid: NodeId, addr: Address) -> anyhow::Result<()> { + let spinner = term::spinner(format!( + "Connecting to {}@{addr}...", + term::format::node(&nid) + )); + if let Err(err) = node.connect(nid, addr.clone()) { + spinner.error(format!( + "Failed to connect to {}@{}: {}", + term::format::node(&nid), + term::format::secondary(addr), + err, + )) + } else { + spinner.finish() + } + Ok(()) +} + +pub fn status(node: &Node) { + if node.is_running() { + term::success!("The node is {}", term::format::positive("running")); + } else { + term::info!("The node is {}", term::format::negative("stopped")); + } +} diff --git a/radicle-cli/src/commands/node/routing.rs b/radicle-cli/src/commands/node/routing.rs new file mode 100644 index 00000000..221356db --- /dev/null +++ b/radicle-cli/src/commands/node/routing.rs @@ -0,0 +1,17 @@ +use radicle::{node::Handle as _, Node}; + +use crate::terminal as term; + +pub fn run(node: &Node) -> anyhow::Result<()> { + let mut t = term::Table::new(term::table::TableOptions::default()); + t.push(["RID", "NID"]); + t.push(["---", "---"]); + for (id, node) in node.routing()? { + t.push([ + term::format::highlight(id).to_string(), + term::format::node(&node), + ]); + } + t.render(); + Ok(()) +} diff --git a/radicle-cli/src/commands/node/tracking.rs b/radicle-cli/src/commands/node/tracking.rs new file mode 100644 index 00000000..61c722ad --- /dev/null +++ b/radicle-cli/src/commands/node/tracking.rs @@ -0,0 +1,49 @@ +use radicle::node::{tracking, Handle as _}; +use radicle::prelude::Did; +use radicle::Node; + +use crate::terminal as term; + +use super::TrackingMode; + +pub fn run(node: &Node, mode: TrackingMode) -> anyhow::Result<()> { + match mode { + TrackingMode::Repos => print_repos(node)?, + TrackingMode::Nodes => print_nodes(node)?, + } + Ok(()) +} + +fn print_repos(node: &Node) -> anyhow::Result<()> { + let mut t = term::Table::new(term::table::TableOptions::default()); + t.push(["RID", "Scope", "Policy"]); + t.push(["---", "-----", "------"]); + for tracking::Repo { id, scope, policy } in node.tracked_repos()? { + t.push([ + term::format::highlight(id.to_string()), + term::format::secondary(scope.to_string()), + term::format::secondary(policy.to_string()), + ]) + } + t.render(); + Ok(()) +} + +fn print_nodes(node: &Node) -> anyhow::Result<()> { + let mut t = term::Table::new(term::table::TableOptions::default()); + t.push(["DID", "Alias", "Policy"]); + t.push(["---", "-----", "------"]); + for tracking::Node { id, alias, policy } in node.tracked_nodes()? { + t.push([ + term::format::highlight(Did::from(id).to_string()), + if alias.is_empty() { + term::format::secondary("n/a".to_string()) + } else { + term::format::secondary(alias) + }, + term::format::secondary(policy.to_string()), + ]); + } + t.render(); + Ok(()) +} diff --git a/radicle-cli/src/main.rs b/radicle-cli/src/main.rs index 6423cffc..0af12555 100644 --- a/radicle-cli/src/main.rs +++ b/radicle-cli/src/main.rs @@ -222,6 +222,14 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option> args.to_vec(), ); } + "node" => { + term::run_command_args::( + rad_node::HELP, + "Node", + rad_node::run, + args.to_vec(), + ); + } "patch" => { term::run_command_args::( rad_patch::HELP, diff --git a/radicle-cli/src/terminal/args.rs b/radicle-cli/src/terminal/args.rs index eeddd054..1d353229 100644 --- a/radicle-cli/src/terminal/args.rs +++ b/radicle-cli/src/terminal/args.rs @@ -3,6 +3,7 @@ use std::str::FromStr; use anyhow::anyhow; use radicle::crypto; +use radicle::node::Address; use radicle::prelude::{Did, Id, NodeId}; #[derive(thiserror::Error, Debug)] @@ -95,3 +96,8 @@ pub fn rid(val: &OsString) -> anyhow::Result { let val = val.to_string_lossy(); Id::from_str(&val).map_err(|_| anyhow!("invalid Repository ID '{}'", val)) } + +pub fn addr(val: &OsString) -> anyhow::Result
{ + let val = val.to_string_lossy(); + Address::from_str(&val).map_err(|_| anyhow!("invalid address '{}'", val)) +}