From e8742f1a2422f515f83731289a8121237894fa5c Mon Sep 17 00:00:00 2001 From: elsirion Date: Thu, 10 Apr 2025 13:18:33 +0200 Subject: [PATCH] feat: implement list nodes command --- src/cli.rs | 2 + src/command/list.rs | 95 +++++++++++++++++++++++++++++++++++++++ src/command/mod.rs | 1 + src/nix/deployment/mod.rs | 8 ++++ src/nix/host/ssh.rs | 2 +- 5 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 src/command/list.rs diff --git a/src/cli.rs b/src/cli.rs index 9068da6..c9649ff 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -210,6 +210,7 @@ attribute set."# #[cfg(debug_assertions)] #[command(about = "Run progress spinner tests", hide = true)] TestProgress, + List(command::list::Opts), #[command(about = "Generate shell auto-completion files (Internal)", hide = true)] GenCompletions { shell: Shell, @@ -331,6 +332,7 @@ pub async fn run() { }; r(command::apply::run(hive, args), opts.config).await } + Command::List(args) => r(command::list::run(hive, args), opts.config).await, Command::GenCompletions { .. } => unreachable!(), } } diff --git a/src/command/list.rs b/src/command/list.rs new file mode 100644 index 0000000..b291277 --- /dev/null +++ b/src/command/list.rs @@ -0,0 +1,95 @@ +use itertools::Itertools; + +use crate::{ + error::ColmenaError, + nix::{node_filter::NodeFilterOpts, Hive}, +}; +use clap::Args; +use serde::Serialize; + +#[derive(Debug, Args)] +#[command(name = "list", about = "List nodes in the Hive")] +pub struct Opts { + #[arg(long, help = "Output node list in JSON format")] + json: bool, + + #[command(flatten)] + node_filter: NodeFilterOpts, +} + +#[derive(Debug, Serialize)] +struct Row { + name: String, + ssh_host: Option, + tags: Vec, +} + +impl Row { + fn table_tags(&self) -> String { + format!("[{}]", self.tags.join(", ")) + } + + fn table_ssh_host(&self) -> String { + self.ssh_host + .clone() + .unwrap_or_else(|| "[no host]".to_owned()) + } + + fn table_name(&self) -> String { + self.name.clone() + } +} + +pub async fn run(hive: Hive, opts: Opts) -> Result<(), ColmenaError> { + let targets = hive + .select_nodes(opts.node_filter.on.clone(), None, false) + .await?; + + let rows = targets + .values() + .sorted_by(|node1, node2| node1.name().cmp(&node2.name())) + .map(|node| Row { + name: node.name().to_owned(), + ssh_host: node + .config() + .to_ssh_host() + .map(|ssh_host| ssh_host.ssh_target()), + tags: node.config().tags().to_vec(), + }) + .collect::>(); + + if opts.json { + println!( + "{}", + serde_json::to_string_pretty(&rows).expect("Failed to serialize nodes") + ); + return Ok(()); + } else { + let max_name_len = rows + .iter() + .map(|row| row.table_name().len()) + .max() + .unwrap_or(0); + let max_ssh_host_len = rows + .iter() + .map(|row| row.table_ssh_host().len()) + .max() + .unwrap_or(0); + let max_tags_len = rows + .iter() + .map(|row| row.table_tags().len()) + .max() + .unwrap_or(0); + + for row in rows { + println!( + "{:max_name_len$} {:max_ssh_host_len$} {:max_tags_len$}", + row.table_name(), + row.table_ssh_host(), + row.table_tags(), + ); + } + } + + Ok(()) +} diff --git a/src/command/mod.rs b/src/command/mod.rs index cc6c8cb..615e057 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -1,6 +1,7 @@ pub mod apply; pub mod eval; pub mod exec; +pub mod list; pub mod nix_info; pub mod repl; diff --git a/src/nix/deployment/mod.rs b/src/nix/deployment/mod.rs index 7b19dca..6762f84 100644 --- a/src/nix/deployment/mod.rs +++ b/src/nix/deployment/mod.rs @@ -89,6 +89,14 @@ impl TargetNode { pub fn into_host(self) -> Option> { self.host } + + pub fn name(&self) -> &str { + &self.name.0 + } + + pub fn config(&self) -> &NodeConfig { + &self.config + } } impl Deployment { diff --git a/src/nix/host/ssh.rs b/src/nix/host/ssh.rs index 64ebb68..8d291bd 100644 --- a/src/nix/host/ssh.rs +++ b/src/nix/host/ssh.rs @@ -251,7 +251,7 @@ impl Ssh { execution.run().await } - fn ssh_target(&self) -> String { + pub(crate) fn ssh_target(&self) -> String { match &self.user { Some(n) => format!("{}@{}", n, self.host), None => self.host.clone(),