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 <fintan.halpenny@gmail.com>
X-Clacks-Overhead: GNU Terry Pratchett
This commit is contained in:
Fintan Halpenny 2023-02-22 13:00:16 +00:00 committed by Alexis Sellier
parent f322305f86
commit ed985f6fe0
No known key found for this signature in database
8 changed files with 288 additions and 0 deletions

View File

@ -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"]

View File

@ -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,

View File

@ -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 <nid> <addr>
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<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
use lexopt::prelude::*;
let mut parser = lexopt::Parser::from_args(args);
let mut op: Option<OperationName> = None;
let mut tracking_mode = TrackingMode::default();
let mut nid: Option<NodeId> = None;
let mut addr: Option<Address> = 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(())
}

View File

@ -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"));
}
}

View File

@ -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(())
}

View File

@ -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(())
}

View File

@ -222,6 +222,14 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
args.to_vec(),
);
}
"node" => {
term::run_command_args::<rad_node::Options, _>(
rad_node::HELP,
"Node",
rad_node::run,
args.to_vec(),
);
}
"patch" => {
term::run_command_args::<rad_patch::Options, _>(
rad_patch::HELP,

View File

@ -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<Id> {
let val = val.to_string_lossy();
Id::from_str(&val).map_err(|_| anyhow!("invalid Repository ID '{}'", val))
}
pub fn addr(val: &OsString) -> anyhow::Result<Address> {
let val = val.to_string_lossy();
Address::from_str(&val).map_err(|_| anyhow!("invalid address '{}'", val))
}