From 9ddc53136a3bf622306b8ab50e90be832bebc35b Mon Sep 17 00:00:00 2001 From: i1i1 Date: Wed, 9 Aug 2023 22:54:17 +0300 Subject: [PATCH 01/20] Add trait implementations for `Goal` --- src/command/apply.rs | 1 + src/command/apply_local.rs | 1 + src/nix/deployment/goal.rs | 42 ++++++++++++++++++++++++++++---------- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/command/apply.rs b/src/command/apply.rs index 1bcf110..626022a 100644 --- a/src/command/apply.rs +++ b/src/command/apply.rs @@ -1,5 +1,6 @@ use std::env; use std::path::PathBuf; +use std::str::FromStr; use clap::{ builder::{ArgPredicate, PossibleValuesParser, ValueParser}, diff --git a/src/command/apply_local.rs b/src/command/apply_local.rs index b712a7e..6111e98 100644 --- a/src/command/apply_local.rs +++ b/src/command/apply_local.rs @@ -1,5 +1,6 @@ use regex::Regex; use std::collections::HashMap; +use std::str::FromStr; use clap::{builder::PossibleValuesParser, Arg, ArgMatches, Command as ClapCommand}; use tokio::fs; diff --git a/src/nix/deployment/goal.rs b/src/nix/deployment/goal.rs index c3c51ea..a7af4d5 100644 --- a/src/nix/deployment/goal.rs +++ b/src/nix/deployment/goal.rs @@ -1,7 +1,9 @@ //! Deployment goals. +use std::str::FromStr; + /// The goal of a deployment. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] pub enum Goal { /// Build the configurations only. Build, @@ -10,6 +12,7 @@ pub enum Goal { Push, /// Make the configuration the boot default and activate now. + #[default] Switch, /// Make the configuration the boot default. @@ -25,20 +28,37 @@ pub enum Goal { UploadKeys, } -impl Goal { - pub fn from_str(s: &str) -> Option { +impl FromStr for Goal { + type Err = &'static str; + fn from_str(s: &str) -> Result { match s { - "build" => Some(Self::Build), - "push" => Some(Self::Push), - "switch" => Some(Self::Switch), - "boot" => Some(Self::Boot), - "test" => Some(Self::Test), - "dry-activate" => Some(Self::DryActivate), - "keys" => Some(Self::UploadKeys), - _ => None, + "build" => Ok(Self::Build), + "push" => Ok(Self::Push), + "switch" => Ok(Self::Switch), + "boot" => Ok(Self::Boot), + "test" => Ok(Self::Test), + "dry-activate" => Ok(Self::DryActivate), + "keys" => Ok(Self::UploadKeys), + _ => Err("Not one of [build, push, switch, boot, test, dry-activate, keys]."), } } +} +impl std::fmt::Display for Goal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Build => "build", + Self::Push => "push", + Self::Switch => "switch", + Self::Boot => "boot", + Self::Test => "test", + Self::DryActivate => "dry-activate", + Self::UploadKeys => "keys", + }) + } +} + +impl Goal { pub fn as_str(&self) -> Option<&'static str> { use Goal::*; match self { From 21df0ac5a5bc9f6b61e9258b0afa875409d25bed Mon Sep 17 00:00:00 2001 From: i1i1 Date: Thu, 10 Aug 2023 00:24:49 +0300 Subject: [PATCH 02/20] Use clap for parsing Hive arguments --- Cargo.lock | 14 +++++ Cargo.toml | 2 +- src/command/apply.rs | 10 +++- src/command/apply_local.rs | 13 ++++- src/command/eval.rs | 10 +++- src/command/exec.rs | 9 ++- src/command/repl.rs | 10 +++- src/nix/flake.rs | 4 +- src/nix/hive/mod.rs | 109 +++++++++++++++++++++++++++++++++- src/util.rs | 117 +++++-------------------------------- 10 files changed, 179 insertions(+), 119 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 49936d5..e20f5c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,6 +171,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34d21f9bf1b425d2968943631ec91202fe5e837264063503708b83013f8fc938" dependencies = [ "clap_builder", + "clap_derive", + "once_cell", ] [[package]] @@ -195,6 +197,18 @@ dependencies = [ "clap", ] +[[package]] +name = "clap_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.16", +] + [[package]] name = "clap_lex" version = "0.4.1" diff --git a/Cargo.toml b/Cargo.toml index d358f2e..7699ca2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ edition = "2021" async-stream = "0.3.5" async-trait = "0.1.68" atty = "0.2" -clap = "4.2.7" +clap = { version = "4.2.7", features = ["derive"] } clap_complete = "4.2.3" clicolors-control = "1" console = "0.15.5" diff --git a/src/command/apply.rs b/src/command/apply.rs index 626022a..ba58e2b 100644 --- a/src/command/apply.rs +++ b/src/command/apply.rs @@ -4,16 +4,16 @@ use std::str::FromStr; use clap::{ builder::{ArgPredicate, PossibleValuesParser, ValueParser}, - value_parser, Arg, ArgMatches, Command as ClapCommand, + value_parser, Arg, ArgMatches, Command as ClapCommand, FromArgMatches, }; -use crate::error::ColmenaError; use crate::nix::deployment::{ Deployment, EvaluationNodeLimit, EvaluatorType, Goal, Options, ParallelismLimit, }; use crate::nix::NodeFilter; use crate::progress::SimpleProgressOutput; use crate::util; +use crate::{error::ColmenaError, nix::hive::HiveArgs}; pub fn register_deploy_args(command: ClapCommand) -> ClapCommand { command @@ -160,7 +160,11 @@ Same as the targets for switch-to-configuration, with the following extra pseudo } pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(), ColmenaError> { - let hive = util::hive_from_args(local_args).await?; + let hive = HiveArgs::from_arg_matches(local_args) + .unwrap() + .into_hive() + .await + .unwrap(); let ssh_config = env::var("SSH_CONFIG_FILE").ok().map(PathBuf::from); diff --git a/src/command/apply_local.rs b/src/command/apply_local.rs index 6111e98..5b01ebd 100644 --- a/src/command/apply_local.rs +++ b/src/command/apply_local.rs @@ -2,14 +2,17 @@ use regex::Regex; use std::collections::HashMap; use std::str::FromStr; -use clap::{builder::PossibleValuesParser, Arg, ArgMatches, Command as ClapCommand}; +use clap::{ + builder::PossibleValuesParser, Arg, ArgMatches, Command as ClapCommand, FromArgMatches, +}; use tokio::fs; use crate::error::ColmenaError; + use crate::nix::deployment::{Deployment, Goal, Options, TargetNode}; +use crate::nix::hive::HiveArgs; use crate::nix::{host::Local as LocalHost, NodeName}; use crate::progress::SimpleProgressOutput; -use crate::util; pub fn subcommand() -> ClapCommand { ClapCommand::new("apply-local") @@ -89,7 +92,11 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<( } } - let hive = util::hive_from_args(local_args).await.unwrap(); + let hive = HiveArgs::from_arg_matches(local_args) + .unwrap() + .into_hive() + .await + .unwrap(); let hostname = { let s = if local_args.contains_id("node") { local_args.get_one::("node").unwrap().to_owned() diff --git a/src/command/eval.rs b/src/command/eval.rs index 0fc271a..0667444 100644 --- a/src/command/eval.rs +++ b/src/command/eval.rs @@ -1,9 +1,9 @@ use std::path::PathBuf; -use clap::{value_parser, Arg, ArgMatches, Command as ClapCommand}; +use clap::{value_parser, Arg, ArgMatches, Command as ClapCommand, FromArgMatches}; use crate::error::ColmenaError; -use crate::util; +use crate::nix::hive::HiveArgs; pub fn subcommand() -> ClapCommand { subcommand_gen("eval") @@ -48,7 +48,11 @@ pub async fn run(global_args: &ArgMatches, local_args: &ArgMatches) -> Result<() ); } - let hive = util::hive_from_args(local_args).await?; + let hive = HiveArgs::from_arg_matches(local_args) + .unwrap() + .into_hive() + .await + .unwrap(); if !(local_args.contains_id("expression") ^ local_args.contains_id("expression_file")) { log::error!("Either an expression (-E) or a .nix file containing an expression should be specified, not both."); diff --git a/src/command/exec.rs b/src/command/exec.rs index b723f6b..05131c5 100644 --- a/src/command/exec.rs +++ b/src/command/exec.rs @@ -2,12 +2,13 @@ use std::env; use std::path::PathBuf; use std::sync::Arc; -use clap::{value_parser, Arg, ArgMatches, Command as ClapCommand}; +use clap::{value_parser, Arg, ArgMatches, Command as ClapCommand, FromArgMatches}; use futures::future::join_all; use tokio::sync::Semaphore; use crate::error::ColmenaError; use crate::job::{JobMonitor, JobState, JobType}; +use crate::nix::hive::HiveArgs; use crate::nix::NodeFilter; use crate::progress::SimpleProgressOutput; use crate::util; @@ -60,7 +61,11 @@ It's recommended to use -- to separate Colmena options from the command to run. } pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(), ColmenaError> { - let hive = util::hive_from_args(local_args).await?; + let hive = HiveArgs::from_arg_matches(local_args) + .unwrap() + .into_hive() + .await + .unwrap(); let ssh_config = env::var("SSH_CONFIG_FILE").ok().map(PathBuf::from); // FIXME: Just get_one:: diff --git a/src/command/repl.rs b/src/command/repl.rs index 5e1e93e..e9c58ad 100644 --- a/src/command/repl.rs +++ b/src/command/repl.rs @@ -1,12 +1,12 @@ use std::io::Write; -use clap::{ArgMatches, Command as ClapCommand}; +use clap::{ArgMatches, Command as ClapCommand, FromArgMatches}; use tempfile::Builder as TempFileBuilder; use tokio::process::Command; use crate::error::ColmenaError; +use crate::nix::hive::HiveArgs; use crate::nix::info::NixCheck; -use crate::util; pub fn subcommand() -> ClapCommand { ClapCommand::new("repl") @@ -24,7 +24,11 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<( let nix_check = NixCheck::detect().await; let nix_version = nix_check.version().expect("Could not detect Nix version"); - let hive = util::hive_from_args(local_args).await?; + let hive = HiveArgs::from_arg_matches(local_args) + .unwrap() + .into_hive() + .await + .unwrap(); let expr = hive.get_repl_expression(); diff --git a/src/nix/flake.rs b/src/nix/flake.rs index 4e7f72d..5f410ea 100644 --- a/src/nix/flake.rs +++ b/src/nix/flake.rs @@ -53,10 +53,10 @@ impl Flake { } /// Creates a flake from a Flake URI. - pub async fn from_uri(uri: String) -> ColmenaResult { + pub async fn from_uri(uri: impl AsRef) -> ColmenaResult { NixCheck::require_flake_support().await?; - let metadata = FlakeMetadata::resolve(&uri).await?; + let metadata = FlakeMetadata::resolve(uri.as_ref()).await?; Ok(Self { metadata, diff --git a/src/nix/hive/mod.rs b/src/nix/hive/mod.rs index 8831cc8..d8a897b 100644 --- a/src/nix/hive/mod.rs +++ b/src/nix/hive/mod.rs @@ -6,7 +6,9 @@ mod tests; use std::collections::HashMap; use std::convert::AsRef; use std::path::{Path, PathBuf}; +use std::str::FromStr; +use clap::Args; use tokio::process::Command; use tokio::sync::OnceCell; use validator::Validate; @@ -16,11 +18,93 @@ use super::{ Flake, MetaConfig, NixExpression, NixFlags, NodeConfig, NodeFilter, NodeName, ProfileDerivation, SerializedNixExpression, StorePath, }; -use crate::error::ColmenaResult; +use crate::error::{ColmenaError, ColmenaResult}; use crate::job::JobHandle; use crate::util::{CommandExecution, CommandExt}; use assets::Assets; +#[derive(Debug, Args)] +pub struct HiveArgs { + #[arg(short = 'f', long, value_name = "CONFIG")] + config: Option, + #[arg(long)] + show_trace: bool, + #[arg(long)] + impure: bool, + #[arg(long, value_parser = crate::util::parse_key_val::)] + nix_option: Vec<(String, String)>, +} + +impl HiveArgs { + pub async fn into_hive(self) -> ColmenaResult { + let path = match self.config { + Some(path) => path, + None => { + // traverse upwards until we find hive.nix + let mut cur = std::env::current_dir()?; + let mut file_path = None; + + loop { + let flake = cur.join("flake.nix"); + if flake.is_file() { + file_path = Some(flake); + break; + } + + let legacy = cur.join("hive.nix"); + if legacy.is_file() { + file_path = Some(legacy); + break; + } + + match cur.parent() { + Some(parent) => { + cur = parent.to_owned(); + } + None => { + break; + } + } + } + + if file_path.is_none() { + log::error!( + "Could not find `hive.nix` or `flake.nix` in {:?} or any parent directory", + std::env::current_dir()? + ); + } + + HivePath::from_path(file_path.unwrap()).await? + } + }; + + match &path { + HivePath::Legacy(p) => { + log::info!("Using configuration: {}", p.to_string_lossy()); + } + HivePath::Flake(flake) => { + log::info!("Using flake: {}", flake.uri()); + } + } + + let mut hive = Hive::new(path).await?; + + if self.show_trace { + hive.set_show_trace(true); + } + + if self.impure { + hive.set_impure(true); + } + + for (name, value) in self.nix_option { + hive.add_nix_option(name, value); + } + + Ok(hive) + } +} + #[derive(Debug, Clone)] pub enum HivePath { /// A Nix Flake. @@ -32,6 +116,29 @@ pub enum HivePath { Legacy(PathBuf), } +impl FromStr for HivePath { + type Err = ColmenaError; + + fn from_str(s: &str) -> Result { + // TODO: check for escaped colon maybe? + + let path = std::path::Path::new(s); + let handle = tokio::runtime::Handle::try_current() + .expect("We should always be executed after we have a runtime"); + + if !path.exists() && s.contains(':') { + // Treat as flake URI + let flake = handle.block_on(Flake::from_uri(s))?; + + log::info!("Using flake: {}", flake.uri()); + + Ok(Self::Flake(flake)) + } else { + handle.block_on(HivePath::from_path(path)) + } + } +} + #[derive(Debug)] pub struct Hive { /// Path to the hive. diff --git a/src/util.rs b/src/util.rs index e131788..93fee77 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,9 +1,10 @@ use std::convert::TryFrom; -use std::path::PathBuf; +use std::error::Error; + use std::process::Stdio; use async_trait::async_trait; -use clap::{parser::ValueSource as ClapValueSource, Arg, ArgMatches, Command as ClapCommand}; +use clap::{Arg, Command as ClapCommand}; use futures::future::join3; use serde::de::DeserializeOwned; use tokio::io::{AsyncBufReadExt, AsyncRead, BufReader}; @@ -12,7 +13,7 @@ use tokio::process::Command; use super::error::{ColmenaError, ColmenaResult}; use super::job::JobHandle; use super::nix::deployment::TargetNodeMap; -use super::nix::{Flake, Hive, HivePath, StorePath}; +use super::nix::StorePath; const NEWLINE: u8 = 0xa; @@ -192,104 +193,18 @@ impl CommandExt for CommandExecution { } } -pub async fn hive_from_args(args: &ArgMatches) -> ColmenaResult { - let path = match args.value_source("config").unwrap() { - ClapValueSource::DefaultValue => { - // traverse upwards until we find hive.nix - let mut cur = std::env::current_dir()?; - let mut file_path = None; - - loop { - let flake = cur.join("flake.nix"); - if flake.is_file() { - file_path = Some(flake); - break; - } - - let legacy = cur.join("hive.nix"); - if legacy.is_file() { - file_path = Some(legacy); - break; - } - - match cur.parent() { - Some(parent) => { - cur = parent.to_owned(); - } - None => { - break; - } - } - } - - if file_path.is_none() { - log::error!( - "Could not find `hive.nix` or `flake.nix` in {:?} or any parent directory", - std::env::current_dir()? - ); - } - - file_path.unwrap() - } - ClapValueSource::CommandLine => { - let path = args - .get_one::("config") - .expect("The config arg should exist") - .to_owned(); - let fpath = PathBuf::from(&path); - - if !fpath.exists() && path.contains(':') { - // Treat as flake URI - let flake = Flake::from_uri(path).await?; - log::info!("Using flake: {}", flake.uri()); - - let hive_path = HivePath::Flake(flake); - - return hive_from_path(hive_path, args).await; - } - - fpath - } - x => panic!("Unexpected value source for config: {:?}", x), - }; - - let hive_path = HivePath::from_path(path).await?; - - hive_from_path(hive_path, args).await -} - -pub async fn hive_from_path(hive_path: HivePath, args: &ArgMatches) -> ColmenaResult { - match &hive_path { - HivePath::Legacy(p) => { - log::info!("Using configuration: {}", p.to_string_lossy()); - } - HivePath::Flake(flake) => { - log::info!("Using flake: {}", flake.uri()); - } - } - - let mut hive = Hive::new(hive_path).await?; - - if args.get_flag("show-trace") { - hive.set_show_trace(true); - } - - if args.get_flag("impure") { - hive.set_impure(true); - } - - if let Some(opts) = args.get_many::("nix-option") { - let iter = opts; - - let names = iter.clone().step_by(2); - let values = iter.clone().skip(1).step_by(2); - - for (name, value) in names.zip(values) { - hive.add_nix_option(name.to_owned(), value.to_owned()); - } - } - - Ok(hive) +/// Parse a single key-value pair +pub fn parse_key_val(s: &str) -> Result<(T, U), Box> +where + T: std::str::FromStr, + T::Err: Error + Send + Sync + 'static, + U: std::str::FromStr, + U::Err: Error + Send + Sync + 'static, +{ + let pos = s + .find('=') + .ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?; + Ok((s[..pos].parse()?, s[pos + 1..].parse()?)) } pub fn register_selector_args(command: ClapCommand) -> ClapCommand { From 1e385824510ca39ef243c4e697278a14659b8246 Mon Sep 17 00:00:00 2001 From: i1i1 Date: Thu, 10 Aug 2023 01:06:00 +0300 Subject: [PATCH 03/20] Parse value for `NodeFilter` with clap --- src/command/apply.rs | 8 ++------ src/nix/node_filter.rs | 11 ++++++++++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/command/apply.rs b/src/command/apply.rs index ba58e2b..e102555 100644 --- a/src/command/apply.rs +++ b/src/command/apply.rs @@ -172,11 +172,7 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<( let goal_arg = local_args.get_one::("goal").unwrap(); let goal = Goal::from_str(goal_arg).unwrap(); - // FIXME: Just get_one:: - let filter = local_args - .get_one::("on") - .map(NodeFilter::new) - .transpose()?; + let filter = local_args.get_one::("on"); if filter.is_none() && goal != Goal::Build { // User did not specify node, we should check meta and see rules @@ -189,7 +185,7 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<( } let targets = hive - .select_nodes(filter, ssh_config, goal.requires_target_host()) + .select_nodes(filter.cloned(), ssh_config, goal.requires_target_host()) .await?; let n_targets = targets.len(); diff --git a/src/nix/node_filter.rs b/src/nix/node_filter.rs index 6f645ca..a796d1e 100644 --- a/src/nix/node_filter.rs +++ b/src/nix/node_filter.rs @@ -3,12 +3,14 @@ use std::collections::HashSet; use std::convert::AsRef; use std::iter::{FromIterator, Iterator}; +use std::str::FromStr; use glob::Pattern as GlobPattern; use super::{ColmenaError, ColmenaResult, NodeConfig, NodeName}; /// A node filter containing a list of rules. +#[derive(Clone)] pub struct NodeFilter { rules: Vec, } @@ -16,7 +18,7 @@ pub struct NodeFilter { /// A filter rule. /// /// The filter rules are OR'd together. -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq)] enum Rule { /// Matches a node's attribute name. MatchName(GlobPattern), @@ -25,6 +27,13 @@ enum Rule { MatchTag(GlobPattern), } +impl FromStr for NodeFilter { + type Err = ColmenaError; + fn from_str(s: &str) -> Result { + Self::new(s) + } +} + impl NodeFilter { /// Creates a new filter using an expression passed using `--on`. pub fn new>(filter: S) -> ColmenaResult { From b80b57cb488b0612f6e1b8b067be0467a78918fb Mon Sep 17 00:00:00 2001 From: i1i1 Date: Wed, 9 Aug 2023 23:24:11 +0300 Subject: [PATCH 04/20] Convert apply-local arguments to type-safe clap derive --- src/command/apply_local.rs | 139 ++++++++++++++++++------------------- src/nix/deployment/goal.rs | 2 +- 2 files changed, 69 insertions(+), 72 deletions(-) diff --git a/src/command/apply_local.rs b/src/command/apply_local.rs index 5b01ebd..29e5df9 100644 --- a/src/command/apply_local.rs +++ b/src/command/apply_local.rs @@ -1,10 +1,7 @@ use regex::Regex; use std::collections::HashMap; -use std::str::FromStr; -use clap::{ - builder::PossibleValuesParser, Arg, ArgMatches, Command as ClapCommand, FromArgMatches, -}; +use clap::{ArgMatches, Args, Command as ClapCommand, FromArgMatches}; use tokio::fs; use crate::error::ColmenaError; @@ -14,57 +11,66 @@ use crate::nix::hive::HiveArgs; use crate::nix::{host::Local as LocalHost, NodeName}; use crate::progress::SimpleProgressOutput; -pub fn subcommand() -> ClapCommand { - ClapCommand::new("apply-local") - .about("Apply configurations on the local machine") - .arg(Arg::new("goal") - .help("Deployment goal") - .long_help("Same as the targets for switch-to-configuration.\n\"push\" is noop in apply-local.") - .default_value("switch") - .index(1) - .value_parser(PossibleValuesParser::new([ - "push", - "switch", - "boot", - "test", - "dry-activate", - "keys", - ]))) - .arg(Arg::new("sudo") - .long("sudo") - .help("Attempt to escalate privileges if not run as root") - .num_args(0)) - .arg(Arg::new("verbose") - .short('v') - .long("verbose") - .help("Be verbose") - .long_help("Deactivates the progress spinner and prints every line of output.") - .num_args(0)) - .arg(Arg::new("no-keys") - .long("no-keys") - .help("Do not deploy keys") - .long_help(r#"Do not deploy secret keys set in `deployment.keys`. +#[derive(Debug, Args)] +#[command( + name = "apply-local", + about = "Apply configurations on the local machine" +)] +pub struct Opts { + #[arg( + help = "Deployment goal", + value_name = "GOAL", + default_value_t, + long_help = "Same as the targets for switch-to-configuration.\n\"push\" is noop in apply-local." + )] + goal: Goal, + #[arg(long, help = "Attempt to escalate privileges if not run as root")] + sudo: bool, + #[arg( + short, + long, + help = "Be verbose", + long_help = "Deactivates the progress spinner and prints every line of output." + )] + verbose: bool, + #[arg( + long, + help = "Do not deploy keys", + long_help = r#"Do not deploy secret keys set in `deployment.keys`. By default, Colmena will deploy keys set in `deployment.keys` before activating the profile on this host. -"#) - .num_args(0)) - .arg(Arg::new("node") - .long("node") - .value_name("NODE") - .help("Override the node name to use") - .num_args(1)) +"# + )] + no_keys: bool, + #[arg(long, help = "Override the node name to use")] + node: Option, + #[arg( + long, + value_name = "COMMAND", + hide = true, + help = "Removed: Configure deployment.privilegeEscalationCommand in node configuration" + )] + sudo_command: Option, + #[command(flatten)] + hive_args: HiveArgs, +} - // Removed - .arg(Arg::new("sudo-command") - .long("sudo-command") - .value_name("COMMAND") - .help("Removed: Configure deployment.privilegeEscalationCommand in node configuration") - .hide(true) - .num_args(1)) +pub fn subcommand() -> ClapCommand { + Opts::augment_args(ClapCommand::new("apply-local")) } pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(), ColmenaError> { - if local_args.contains_id("sudo-command") { + let Opts { + goal, + sudo, + verbose, + no_keys, + node, + sudo_command, + hive_args, + } = Opts::from_arg_matches(local_args).expect("Failed to parse `apply-local` options."); + + if sudo_command.is_some() { log::error!("--sudo-command has been removed. Please configure it in deployment.privilegeEscalationCommand in the node configuration."); quit::with_code(1); } @@ -81,35 +87,26 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<( quit::with_code(5); } - let escalate_privileges = local_args.get_flag("sudo"); - let verbose = local_args.get_flag("verbose") || escalate_privileges; // cannot use spinners with interactive sudo + let verbose = verbose || sudo; // cannot use spinners with interactive sudo { let euid: u32 = unsafe { libc::geteuid() }; - if euid != 0 && !escalate_privileges { + if euid != 0 && !sudo { log::warn!("Colmena was not started by root. This is probably not going to work."); log::warn!("Hint: Add the --sudo flag."); } } - let hive = HiveArgs::from_arg_matches(local_args) - .unwrap() + let hive = hive_args .into_hive() .await - .unwrap(); - let hostname = { - let s = if local_args.contains_id("node") { - local_args.get_one::("node").unwrap().to_owned() - } else { - hostname::get() - .expect("Could not get hostname") - .to_string_lossy() - .into_owned() - }; - - NodeName::new(s)? - }; - let goal = Goal::from_str(local_args.get_one::("goal").unwrap()).unwrap(); + .expect("Failed to get hive from arguments"); + let hostname = NodeName::new(node.unwrap_or_else(|| { + hostname::get() + .expect("Could not get hostname") + .to_string_lossy() + .into_owned() + }))?; let target = { if let Some(info) = hive.deployment_info_single(&hostname).await.unwrap() { @@ -123,7 +120,7 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<( quit::with_code(2); } let mut host = LocalHost::new(nix_options); - if escalate_privileges { + if sudo { let command = info.privilege_escalation_command().to_owned(); host.set_privilege_escalation_command(Some(command)); } @@ -148,13 +145,13 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<( let options = { let mut options = Options::default(); - options.set_upload_keys(!local_args.get_flag("no-keys")); + options.set_upload_keys(!no_keys); options }; deployment.set_options(options); - let (deployment, output) = tokio::join!(deployment.execute(), output.run_until_completion(),); + let (deployment, output) = tokio::join!(deployment.execute(), output.run_until_completion()); deployment?; output?; diff --git a/src/nix/deployment/goal.rs b/src/nix/deployment/goal.rs index a7af4d5..0ff89ac 100644 --- a/src/nix/deployment/goal.rs +++ b/src/nix/deployment/goal.rs @@ -3,7 +3,7 @@ use std::str::FromStr; /// The goal of a deployment. -#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, clap::ValueEnum)] pub enum Goal { /// Build the configurations only. Build, From 87f4e3a6768174cdce1dc99c3890fd3335f97eb0 Mon Sep 17 00:00:00 2001 From: i1i1 Date: Thu, 10 Aug 2023 01:23:57 +0300 Subject: [PATCH 05/20] Convert apply arguments to type-safe clap derive --- src/command/apply.rs | 303 ++++++++++++++++------------------ src/command/build.rs | 8 +- src/command/upload_keys.rs | 8 +- src/nix/deployment/limits.rs | 30 +++- src/nix/deployment/options.rs | 27 ++- src/nix/node_filter.rs | 21 ++- 6 files changed, 210 insertions(+), 187 deletions(-) diff --git a/src/command/apply.rs b/src/command/apply.rs index e102555..b81768f 100644 --- a/src/command/apply.rs +++ b/src/command/apply.rs @@ -1,135 +1,126 @@ use std::env; use std::path::PathBuf; -use std::str::FromStr; -use clap::{ - builder::{ArgPredicate, PossibleValuesParser, ValueParser}, - value_parser, Arg, ArgMatches, Command as ClapCommand, FromArgMatches, -}; +use clap::{builder::ArgPredicate, ArgMatches, Args, Command as ClapCommand, FromArgMatches}; -use crate::nix::deployment::{ - Deployment, EvaluationNodeLimit, EvaluatorType, Goal, Options, ParallelismLimit, +use crate::nix::{ + deployment::{Deployment, EvaluationNodeLimit, EvaluatorType, Goal, Options, ParallelismLimit}, + node_filter::NodeFilterOpts, }; -use crate::nix::NodeFilter; use crate::progress::SimpleProgressOutput; -use crate::util; + use crate::{error::ColmenaError, nix::hive::HiveArgs}; -pub fn register_deploy_args(command: ClapCommand) -> ClapCommand { - command - .arg(Arg::new("eval-node-limit") - .long("eval-node-limit") - .value_name("LIMIT") - .help("Evaluation node limit") - .long_help(r#"Limits the maximum number of hosts to be evaluated at once. +#[derive(Debug, Args)] +pub struct DeployOpts { + #[arg( + value_name = "LIMIT", + default_value_t, + long, + help = "Evaluation node limit", + long_help = r#"Limits the maximum number of hosts to be evaluated at once. The evaluation process is RAM-intensive. The default behavior is to limit the maximum number of host evaluated at the same time based on naive heuristics. Set to 0 to disable the limit. -"#) - .default_value("auto") - .num_args(1) - .value_parser(ValueParser::new(|s: &str| -> Result { - if s == "auto" { - return Ok(EvaluationNodeLimit::Heuristic); - } +"# + )] + eval_node_limit: EvaluationNodeLimit, + #[arg( + value_name = "LIMIT", + default_value_t = 10, + long, + short, + help = "Deploy parallelism limit", + long_help = r#"Limits the maximum number of hosts to be deployed in parallel. - match s.parse::() { - Ok(0) => Ok(EvaluationNodeLimit::None), - Ok(n) => Ok(EvaluationNodeLimit::Manual(n)), - Err(_) => Err(String::from("The value must be a valid number")), - } - }))) - .arg(Arg::new("parallel") - .short('p') - .long("parallel") - .value_name("LIMIT") - .help("Deploy parallelism limit") - .long_help(r#"Limits the maximum number of hosts to be deployed in parallel. - -Set to 0 to disable parallemism limit. -"#) - .default_value("10") - .num_args(1) - .value_parser(value_parser!(usize))) - .arg(Arg::new("keep-result") - .long("keep-result") - .help("Create GC roots for built profiles") - .long_help(r#"Create GC roots for built profiles. +Set to 0 to disable parallelism limit. +"# + )] + parallel: usize, + #[arg( + long, + help = "Create GC roots for built profiles", + long_help = r#"Create GC roots for built profiles. The built system profiles will be added as GC roots so that they will not be removed by the garbage collector. The links will be created under .gcroots in the directory the Hive configuration is located. -"#) - .num_args(0)) - .arg(Arg::new("verbose") - .short('v') - .long("verbose") - .help("Be verbose") - .long_help("Deactivates the progress spinner and prints every line of output.") - .num_args(0)) - .arg(Arg::new("no-keys") - .long("no-keys") - .help("Do not upload keys") - .long_help(r#"Do not upload secret keys set in `deployment.keys`. +"# + )] + keep_result: bool, + #[arg( + short, + long, + help = "Be verbose", + long_help = "Deactivates the progress spinner and prints every line of output." + )] + verbose: bool, + #[arg( + long, + help = "Do not upload keys", + long_help = r#"Do not upload secret keys set in `deployment.keys`. By default, Colmena will upload keys set in `deployment.keys` before deploying the new profile on a node. To upload keys without building or deploying the rest of the configuration, use `colmena upload-keys`. -"#) - .num_args(0)) - .arg(Arg::new("reboot") - .long("reboot") - .help("Reboot nodes after activation") - .long_help("Reboots nodes after activation and waits for them to come back up.") - .num_args(0)) - .arg(Arg::new("no-substitute") - .long("no-substitute") - .alias("no-substitutes") - .help("Do not use substitutes") - .long_help("Disables the use of substituters when copying closures to the remote host.") - .num_args(0)) - .arg(Arg::new("no-gzip") - .long("no-gzip") - .help("Do not use gzip") - .long_help("Disables the use of gzip when copying closures to the remote host.") - .num_args(0)) - .arg(Arg::new("build-on-target") - .long("build-on-target") - .help("Build the system profiles on the target nodes") - .long_help(r#"Build the system profiles on the target nodes themselves. +"# + )] + no_keys: bool, + #[arg( + long, + help = "Reboot nodes after activation", + long_help = "Reboots nodes after activation and waits for them to come back up." + )] + reboot: bool, + #[arg( + long, + alias = "no-substitutes", + help = "Do not use substitutes", + long_help = "Disables the use of substituters when copying closures to the remote host." + )] + no_substitute: bool, + #[arg( + long, + help = "Do not use gzip", + long_help = "Disables the use of gzip when copying closures to the remote host." + )] + no_gzip: bool, + #[arg( + long, + help = "Build the system profiles on the target nodes", + long_help = r#"Build the system profiles on the target nodes themselves. If enabled, the system profiles will be built on the target nodes themselves, not on the host running Colmena itself. This overrides per-node perferences set in `deployment.buildOnTarget`. To temporarily disable remote build on all nodes, use `--no-build-on-target`. -"#) - .num_args(0)) - .arg(Arg::new("no-build-on-target") - .long("no-build-on-target") - .hide(true) - .num_args(0)) - .arg(Arg::new("force-replace-unknown-profiles") - .long("force-replace-unknown-profiles") - .help("Ignore all targeted nodes deployment.replaceUnknownProfiles setting") - .long_help(r#"If `deployment.replaceUnknownProfiles` is set for a target, using this switch -will treat deployment.replaceUnknownProfiles as though it was set true and perform unknown profile replacement."#) - .num_args(0)) - .arg(Arg::new("evaluator") - .long("evaluator") - .help("The evaluator to use (experimental)") - .long_help(r#"If set to `chunked` (default), evaluation of nodes will happen in batches. If set to `streaming`, the experimental streaming evaluator (nix-eval-jobs) will be used and nodes will be evaluated in parallel. +"# + )] + build_on_target: bool, + #[arg(long, hide = true)] + no_build_on_target: bool, + #[arg( + long, + help = "Ignore all targeted nodes deployment.replaceUnknownProfiles setting", + long_help = r#"If `deployment.replaceUnknownProfiles` is set for a target, using this switch +will treat deployment.replaceUnknownProfiles as though it was set true and perform unknown profile replacement."# + )] + force_replace_unknown_profiles: bool, + #[arg( + long, + default_value_t, + help = "The evaluator to use (experimental)", + long_help = r#"If set to `chunked` (default), evaluation of nodes will happen in batches. If set to `streaming`, the experimental streaming evaluator (nix-eval-jobs) will be used and nodes will be evaluated in parallel. -This is an experimental feature."#) - .default_value("chunked") - .value_parser(value_parser!(EvaluatorType))) +This is an experimental feature."# + )] + evaluator: EvaluatorType, } -pub fn subcommand() -> ClapCommand { - let command = ClapCommand::new("apply") - .about("Apply configurations on remote machines") - .arg( - Arg::new("goal") - .help("Deployment goal") - .long_help( - r#"The goal of the deployment. +#[derive(Debug, Args)] +#[command(name = "apply", about = "Apply configurations on remote machines")] +struct Opts { + #[arg( + help = "Deployment goal", + long_help = r#"The goal of the deployment. Same as the targets for switch-to-configuration, with the following extra pseudo-goals: @@ -139,24 +130,17 @@ Same as the targets for switch-to-configuration, with the following extra pseudo `switch` is the default goal unless `--reboot` is passed, in which case `boot` is the default. "#, - ) - .default_value("switch") - .default_value_if("reboot", ArgPredicate::IsPresent, Some("boot")) - .default_value("switch") - .index(1) - .value_parser(PossibleValuesParser::new([ - "build", - "push", - "switch", - "boot", - "test", - "dry-activate", - "keys", - ])), - ); - let command = register_deploy_args(command); + default_value_if("reboot", ArgPredicate::IsPresent, Some("boot")) + )] + goal: Goal, + #[command(flatten)] + deploy: DeployOpts, + #[command(flatten)] + node_filter: NodeFilterOpts, +} - util::register_selector_args(command) +pub fn subcommand() -> ClapCommand { + Opts::augment_args(ClapCommand::new("apply")) } pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(), ColmenaError> { @@ -168,13 +152,27 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<( let ssh_config = env::var("SSH_CONFIG_FILE").ok().map(PathBuf::from); - // FIXME: Just get_one:: - let goal_arg = local_args.get_one::("goal").unwrap(); - let goal = Goal::from_str(goal_arg).unwrap(); + let Opts { + goal, + deploy: + DeployOpts { + eval_node_limit, + parallel, + keep_result, + verbose, + no_keys, + reboot, + no_substitute, + no_gzip, + build_on_target, + no_build_on_target, + force_replace_unknown_profiles, + evaluator, + }, + node_filter, + } = Opts::from_arg_matches(local_args).expect("Failed to parse `apply` args"); - let filter = local_args.get_one::("on"); - - if filter.is_none() && goal != Goal::Build { + if node_filter.on.is_none() && goal != Goal::Build { // User did not specify node, we should check meta and see rules let meta = hive.get_meta_config().await?; if !meta.allow_apply_all { @@ -185,11 +183,15 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<( } let targets = hive - .select_nodes(filter.cloned(), ssh_config, goal.requires_target_host()) + .select_nodes( + node_filter.on.clone(), + ssh_config, + goal.requires_target_host(), + ) .await?; let n_targets = targets.len(); - let verbose = local_args.get_flag("verbose") || goal == Goal::DryActivate; + let verbose = verbose || goal == Goal::DryActivate; let mut output = SimpleProgressOutput::new(verbose); let progress = output.get_sender(); @@ -198,27 +200,20 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<( // FIXME: Configure limits let options = { let mut options = Options::default(); - options.set_substituters_push(!local_args.get_flag("no-substitute")); - options.set_gzip(!local_args.get_flag("no-gzip")); - options.set_upload_keys(!local_args.get_flag("no-keys")); - options.set_reboot(local_args.get_flag("reboot")); - options.set_force_replace_unknown_profiles( - local_args.get_flag("force-replace-unknown-profiles"), - ); - options.set_evaluator( - local_args - .get_one::("evaluator") - .unwrap() - .to_owned(), - ); + options.set_substituters_push(!no_substitute); + options.set_gzip(!no_gzip); + options.set_upload_keys(!no_keys); + options.set_reboot(reboot); + options.set_force_replace_unknown_profiles(force_replace_unknown_profiles); + options.set_evaluator(evaluator); - if local_args.get_flag("keep-result") { + if keep_result { options.set_create_gc_roots(true); } - if local_args.get_flag("no-build-on-target") { + if no_build_on_target { options.set_force_build_on_target(false); - } else if local_args.get_flag("build-on-target") { + } else if build_on_target { options.set_force_build_on_target(true); } @@ -227,7 +222,7 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<( deployment.set_options(options); - if local_args.get_flag("no-keys") && goal == Goal::UploadKeys { + if no_keys && goal == Goal::UploadKeys { log::error!("--no-keys cannot be used when the goal is to upload keys"); quit::with_code(1); } @@ -235,23 +230,17 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<( let parallelism_limit = { let mut limit = ParallelismLimit::default(); limit.set_apply_limit({ - let limit = local_args.get_one::("parallel").unwrap().to_owned(); - if limit == 0 { + if parallel == 0 { n_targets } else { - limit + parallel } }); limit }; - let evaluation_node_limit = local_args - .get_one::("eval-node-limit") - .unwrap() - .to_owned(); - deployment.set_parallelism_limit(parallelism_limit); - deployment.set_evaluation_node_limit(evaluation_node_limit); + deployment.set_evaluation_node_limit(eval_node_limit); let (deployment, output) = tokio::join!(deployment.execute(), output.run_until_completion(),); diff --git a/src/command/build.rs b/src/command/build.rs index 286b42b..8bbf4fa 100644 --- a/src/command/build.rs +++ b/src/command/build.rs @@ -1,9 +1,9 @@ -use clap::{builder::PossibleValuesParser, Arg, Command as ClapCommand}; +use clap::{builder::PossibleValuesParser, Arg, Args, Command as ClapCommand}; use crate::util; -use super::apply; pub use super::apply::run; +use super::apply::DeployOpts; pub fn subcommand() -> ClapCommand { let command = ClapCommand::new("build") @@ -21,7 +21,5 @@ This subcommand behaves as if you invoked `apply` with the `build` goal."#, .num_args(1), ); - let command = apply::register_deploy_args(command); - - util::register_selector_args(command) + util::register_selector_args(DeployOpts::augment_args_for_update(command)) } diff --git a/src/command/upload_keys.rs b/src/command/upload_keys.rs index 28d9944..16555d4 100644 --- a/src/command/upload_keys.rs +++ b/src/command/upload_keys.rs @@ -1,9 +1,9 @@ -use clap::{builder::PossibleValuesParser, Arg, Command as ClapCommand}; +use clap::{builder::PossibleValuesParser, Arg, Args, Command as ClapCommand}; use crate::util; -use super::apply; pub use super::apply::run; +use super::apply::DeployOpts; pub fn subcommand() -> ClapCommand { let command = ClapCommand::new("upload-keys") @@ -21,7 +21,5 @@ This subcommand behaves as if you invoked `apply` with the pseudo `keys` goal."# .num_args(1), ); - let command = apply::register_deploy_args(command); - - util::register_selector_args(command) + util::register_selector_args(DeployOpts::augment_args_for_update(command)) } diff --git a/src/nix/deployment/limits.rs b/src/nix/deployment/limits.rs index 5b2d402..27202f6 100644 --- a/src/nix/deployment/limits.rs +++ b/src/nix/deployment/limits.rs @@ -1,5 +1,7 @@ //! Parallelism limits. +use std::str::FromStr; + use tokio::sync::Semaphore; /// Amount of RAM reserved for the system, in MB. @@ -55,9 +57,10 @@ impl ParallelismLimit { /// - A simple heuristic based on remaining memory in the system /// - A supplied number /// - No limit at all -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, Default)] pub enum EvaluationNodeLimit { /// Use a naive heuristic based on available memory. + #[default] Heuristic, /// Supply the maximum number of nodes. @@ -67,9 +70,28 @@ pub enum EvaluationNodeLimit { None, } -impl Default for EvaluationNodeLimit { - fn default() -> Self { - Self::Heuristic +impl FromStr for EvaluationNodeLimit { + type Err = &'static str; + fn from_str(s: &str) -> Result { + if s == "auto" { + return Ok(EvaluationNodeLimit::Heuristic); + } + + match s.parse::() { + Ok(0) => Ok(EvaluationNodeLimit::None), + Ok(n) => Ok(EvaluationNodeLimit::Manual(n)), + Err(_) => Err("The value must be a valid number or `auto`"), + } + } +} + +impl std::fmt::Display for EvaluationNodeLimit { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Heuristic => write!(f, "auto"), + Self::None => write!(f, "0"), + Self::Manual(n) => write!(f, "{n}"), + } } } diff --git a/src/nix/deployment/options.rs b/src/nix/deployment/options.rs index 768bd22..936de4d 100644 --- a/src/nix/deployment/options.rs +++ b/src/nix/deployment/options.rs @@ -1,6 +1,6 @@ //! Deployment options. -use clap::{builder::PossibleValue, ValueEnum}; +use clap::ValueEnum; use crate::nix::CopyOptions; @@ -36,12 +36,22 @@ pub struct Options { } /// Which evaluator to use. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Default, ValueEnum)] pub enum EvaluatorType { + #[default] Chunked, Streaming, } +impl std::fmt::Display for EvaluatorType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Chunked => "chunked", + Self::Streaming => "streaming", + }) + } +} + impl Options { pub fn set_substituters_push(&mut self, value: bool) { self.substituters_push = value; @@ -98,16 +108,3 @@ impl Default for Options { } } } - -impl ValueEnum for EvaluatorType { - fn value_variants<'a>() -> &'a [Self] { - &[Self::Chunked, Self::Streaming] - } - - fn to_possible_value<'a>(&self) -> Option { - match self { - Self::Chunked => Some(PossibleValue::new("chunked")), - Self::Streaming => Some(PossibleValue::new("streaming")), - } - } -} diff --git a/src/nix/node_filter.rs b/src/nix/node_filter.rs index a796d1e..f018cd8 100644 --- a/src/nix/node_filter.rs +++ b/src/nix/node_filter.rs @@ -5,12 +5,31 @@ use std::convert::AsRef; use std::iter::{FromIterator, Iterator}; use std::str::FromStr; +use clap::Args; use glob::Pattern as GlobPattern; use super::{ColmenaError, ColmenaResult, NodeConfig, NodeName}; +#[derive(Debug, Args)] +pub struct NodeFilterOpts { + #[arg( + long, + value_name = "NODES", + help = "Node selector", + long_help = r#"Select a list of nodes to deploy to. + +The list is comma-separated and globs are supported. To match tags, prepend the filter by @. Valid examples: + +- host1,host2,host3 +- edge-* +- edge-*,core-* +- @a-tag,@tags-can-have-*"# + )] + pub on: Option, +} + /// A node filter containing a list of rules. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct NodeFilter { rules: Vec, } From cdfc1f15a1727e895dc455c0d2f05a18c4719528 Mon Sep 17 00:00:00 2001 From: i1i1 Date: Thu, 10 Aug 2023 01:28:41 +0300 Subject: [PATCH 06/20] Convert build arguments to type-safe clap derive --- src/command/build.rs | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/command/build.rs b/src/command/build.rs index 8bbf4fa..170c435 100644 --- a/src/command/build.rs +++ b/src/command/build.rs @@ -1,25 +1,25 @@ -use clap::{builder::PossibleValuesParser, Arg, Args, Command as ClapCommand}; +use clap::{Args, Command as ClapCommand}; -use crate::util; +use crate::nix::Goal; pub use super::apply::run; use super::apply::DeployOpts; -pub fn subcommand() -> ClapCommand { - let command = ClapCommand::new("build") - .about("Build configurations but not push to remote machines") - .long_about( - r#"Build configurations but not push to remote machines +#[derive(Debug, Args)] +#[command( + name = "build", + about = "Build configurations but not push to remote machines", + long_about = r#"Build configurations but not push to remote machines -This subcommand behaves as if you invoked `apply` with the `build` goal."#, - ) - .arg( - Arg::new("goal") - .hide(true) - .default_value("build") - .value_parser(PossibleValuesParser::new(["build"])) - .num_args(1), - ); - - util::register_selector_args(DeployOpts::augment_args_for_update(command)) +This subcommand behaves as if you invoked `apply` with the `build` goal."# +)] +pub struct Opts { + #[command(flatten)] + deploy: DeployOpts, + #[arg(hide = true, default_value_t = Goal::Build)] + goal: Goal, +} + +pub fn subcommand() -> ClapCommand { + Opts::augment_args(ClapCommand::new("build")) } From 5b8611744b844eee3568849910ba6edc0efda7c6 Mon Sep 17 00:00:00 2001 From: i1i1 Date: Thu, 10 Aug 2023 01:47:10 +0300 Subject: [PATCH 07/20] Convert eval arguments to type-safe clap derive --- src/cli.rs | 3 +- src/command/eval.rs | 95 +++++++++++++++++++++------------------------ 2 files changed, 45 insertions(+), 53 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index c6246bd..e424d93 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -191,8 +191,7 @@ It's also possible to specify the preference using environment variables. See ClapCommand { - subcommand_gen("eval") -} - -pub fn deprecated_alias() -> ClapCommand { - subcommand_gen("introspect").hide(true) -} - -fn subcommand_gen(name: &'static str) -> ClapCommand { - ClapCommand::new(name) - .about("Evaluate an expression using the complete configuration") - .long_about(r#"Evaluate an expression using the complete configuration +#[derive(Debug, Args)] +#[command( + name = "eval", + alias = "introspect", + about = "Evaluate an expression using the complete configuration", + long_about = r#"Evaluate an expression using the complete configuration Your expression should take an attribute set with keys `pkgs`, `lib` and `nodes` (like a NixOS module) and return a JSON-serializable value. For example, to retrieve the configuration of one node, you may write something like: { nodes, ... }: nodes.node-a.config.networking.hostName -"#) - .arg(Arg::new("expression_file") - .index(1) - .value_name("FILE") - .help("The .nix file containing the expression") - .num_args(1) - .value_parser(value_parser!(PathBuf))) - .arg(Arg::new("expression") - .short('E') - .value_name("EXPRESSION") - .help("The Nix expression") - .num_args(1)) - .arg(Arg::new("instantiate") - .long("instantiate") - .help("Actually instantiate the expression") - .num_args(0)) +"# +)] +pub struct Opts { + #[arg(short = 'E', value_name = "EXPRESSION", help = "The Nix expression")] + expression: Option, + #[arg(long, help = "Actually instantiate the expression")] + instantiate: bool, + #[arg( + value_name = "FILE", + help = "The .nix file containing the expression", + conflicts_with("expression") + )] + expression_file: Option, +} + +pub fn subcommand() -> ClapCommand { + Opts::augment_args(ClapCommand::new("eval")) } pub async fn run(global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(), ColmenaError> { @@ -54,31 +49,29 @@ pub async fn run(global_args: &ArgMatches, local_args: &ArgMatches) -> Result<() .await .unwrap(); - if !(local_args.contains_id("expression") ^ local_args.contains_id("expression_file")) { - log::error!("Either an expression (-E) or a .nix file containing an expression should be specified, not both."); - quit::with_code(1); - } + let Opts { + instantiate, + expression, + expression_file, + } = Opts::from_arg_matches(local_args).expect("Failed to parse args"); - let expression = if local_args.contains_id("expression") { - local_args - .get_one::("expression") - .unwrap() - .to_owned() - } else { - let path = local_args - .get_one::("expression_file") - .unwrap() - .to_owned(); - format!( - "import {}", - path.canonicalize() - .expect("Could not generate absolute path to expression file.") - .to_str() - .unwrap() - ) + let expression = expression_file + .map(|path| { + format!( + "import {}", + path.canonicalize() + .expect("Could not generate absolute path to expression file.") + .to_str() + .unwrap() + ) + }) + .or(expression); + + let Some(expression) = expression else { + log::error!("Provide either an expression (-E) or a .nix file containing an expression."); + quit::with_code(1); }; - let instantiate = local_args.get_flag("instantiate"); let result = hive.introspect(expression, instantiate).await?; if instantiate { From 367c253a471746a183ba5f1cd9b6c1e6e60128a8 Mon Sep 17 00:00:00 2001 From: i1i1 Date: Thu, 10 Aug 2023 01:55:11 +0300 Subject: [PATCH 08/20] Convert exec arguments to type-safe clap derive --- src/command/exec.rs | 113 ++++++++++++++++++++------------------------ 1 file changed, 50 insertions(+), 63 deletions(-) diff --git a/src/command/exec.rs b/src/command/exec.rs index 05131c5..6410966 100644 --- a/src/command/exec.rs +++ b/src/command/exec.rs @@ -2,62 +2,58 @@ use std::env; use std::path::PathBuf; use std::sync::Arc; -use clap::{value_parser, Arg, ArgMatches, Command as ClapCommand, FromArgMatches}; +use clap::{ArgMatches, Args, Command as ClapCommand, FromArgMatches}; use futures::future::join_all; use tokio::sync::Semaphore; use crate::error::ColmenaError; use crate::job::{JobMonitor, JobState, JobType}; use crate::nix::hive::HiveArgs; -use crate::nix::NodeFilter; +use crate::nix::node_filter::NodeFilterOpts; use crate::progress::SimpleProgressOutput; use crate::util; -pub fn subcommand() -> ClapCommand { - let command = ClapCommand::new("exec") - .about("Run a command on remote machines") - .arg( - Arg::new("parallel") - .short('p') - .long("parallel") - .value_name("LIMIT") - .help("Deploy parallelism limit") - .long_help( - r#"Limits the maximum number of hosts to run the command in parallel. +#[derive(Debug, Args)] +#[command(name = "exec", about = "Run a command on remote machines")] +struct Opts { + #[arg( + short, + long, + default_value_t = 0, + value_name = "LIMIT", + help = "Deploy parallelism limit", + long_help = r#"Limits the maximum number of hosts to run the command in parallel. In `colmena exec`, the parallelism limit is disabled (0) by default. -"#, - ) - .default_value("0") - .num_args(1) - .value_parser(value_parser!(usize)), - ) - .arg( - Arg::new("verbose") - .short('v') - .long("verbose") - .help("Be verbose") - .long_help("Deactivates the progress spinner and prints every line of output.") - .num_args(0), - ) - .arg( - Arg::new("command") - .value_name("COMMAND") - .trailing_var_arg(true) - .help("Command") - .required(true) - .num_args(1..) - .long_help( - r#"Command to run +"# + )] + parallel: usize, + #[arg( + short, + long, + help = "Be verbose", + long_help = "Deactivates the progress spinner and prints every line of output." + )] + verbose: bool, + #[command(flatten)] + nodes: NodeFilterOpts, + #[arg( + trailing_var_arg = true, + required = true, + value_name = "COMMAND", + help = "Command", + long_help = r#"Command to run It's recommended to use -- to separate Colmena options from the command to run. For example: colmena exec --on @routers -- tcpdump -vni any ip[9] == 89 -"#, - ), - ); +"# + )] + command: Vec, +} - util::register_selector_args(command) +pub fn subcommand() -> ClapCommand { + Opts::augment_args(ClapCommand::new("exec")) } pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(), ColmenaError> { @@ -68,33 +64,24 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<( .unwrap(); let ssh_config = env::var("SSH_CONFIG_FILE").ok().map(PathBuf::from); - // FIXME: Just get_one:: - let filter = local_args - .get_one::("on") - .map(NodeFilter::new) - .transpose()?; + let Opts { + parallel, + verbose, + nodes, + command, + } = Opts::from_arg_matches(local_args).unwrap(); - let mut targets = hive.select_nodes(filter, ssh_config, true).await?; + let mut targets = hive.select_nodes(nodes.on, ssh_config, true).await?; - let parallel_sp = Arc::new({ - let limit = local_args.get_one::("parallel").unwrap().to_owned(); - - if limit > 0 { - Some(Semaphore::new(limit)) - } else { - None - } + let parallel_sp = Arc::new(if parallel > 0 { + Some(Semaphore::new(parallel)) + } else { + None }); - let command: Arc> = Arc::new( - local_args - .get_many::("command") - .unwrap() - .cloned() - .collect(), - ); + let command = Arc::new(command); - let mut output = SimpleProgressOutput::new(local_args.get_flag("verbose")); + let mut output = SimpleProgressOutput::new(verbose); let (mut monitor, meta) = JobMonitor::new(output.get_sender()); @@ -107,7 +94,7 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<( for (name, target) in targets.drain() { let parallel_sp = parallel_sp.clone(); - let command = command.clone(); + let command = Arc::clone(&command); let mut host = target.into_host().unwrap(); From d55555c1ed6ad8420fe51ebcff1b29bf9f77a505 Mon Sep 17 00:00:00 2001 From: i1i1 Date: Thu, 10 Aug 2023 01:56:36 +0300 Subject: [PATCH 09/20] Convert nix-info arguments to type-safe clap derive --- src/command/nix_info.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/command/nix_info.rs b/src/command/nix_info.rs index a4c0643..8857d5b 100644 --- a/src/command/nix_info.rs +++ b/src/command/nix_info.rs @@ -1,14 +1,22 @@ -use clap::{ArgMatches, Command as ClapCommand}; +use clap::{ArgMatches, Args, FromArgMatches, Command}; use crate::error::ColmenaError; use crate::nix::evaluator::nix_eval_jobs::get_pinned_nix_eval_jobs; use crate::nix::NixCheck; -pub fn subcommand() -> ClapCommand { - ClapCommand::new("nix-info").about("Show information about the current Nix installation") +#[derive(Debug, Args)] +#[command( + name = "nix-info", + about = "Show information about the current Nix installation" +)] +pub struct Opts {} + +pub fn subcommand() -> Command { + Opts::augment_args(Command::new("nix-info")) } -pub async fn run(_global_args: &ArgMatches, _local_args: &ArgMatches) -> Result<(), ColmenaError> { +pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(), ColmenaError> { + let Opts {} = Opts::from_arg_matches(local_args).unwrap(); let check = NixCheck::detect().await; check.print_version_info(); check.print_flakes_info(false); From ed23d1da6711f74ec86154abcfa3d1cec6899a43 Mon Sep 17 00:00:00 2001 From: i1i1 Date: Thu, 10 Aug 2023 01:58:46 +0300 Subject: [PATCH 10/20] Convert `repl` arguments to type-safe clap derive --- src/command/repl.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/command/repl.rs b/src/command/repl.rs index e9c58ad..c132cdc 100644 --- a/src/command/repl.rs +++ b/src/command/repl.rs @@ -1,6 +1,6 @@ use std::io::Write; -use clap::{ArgMatches, Command as ClapCommand, FromArgMatches}; +use clap::{ArgMatches, Command as ClapCommand, FromArgMatches, Args}; use tempfile::Builder as TempFileBuilder; use tokio::process::Command; @@ -8,19 +8,26 @@ use crate::error::ColmenaError; use crate::nix::hive::HiveArgs; use crate::nix::info::NixCheck; -pub fn subcommand() -> ClapCommand { - ClapCommand::new("repl") - .about("Start an interactive REPL with the complete configuration") - .long_about( +#[derive(Debug, Args)] +#[command( + name = "repl", + about = "Start an interactive REPL with the complete configuration", + long_about = r#"Start an interactive REPL with the complete configuration In the REPL, you can inspect the configuration interactively with tab completion. The node configurations are accessible under the `nodes` attribute set."#, - ) +)] +pub struct Opts {} + +pub fn subcommand() -> ClapCommand { + Opts::augment_args(ClapCommand::new("repl")) } pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(), ColmenaError> { + let Opts {} = Opts::from_arg_matches(local_args).unwrap(); + let nix_check = NixCheck::detect().await; let nix_version = nix_check.version().expect("Could not detect Nix version"); From ce0782ccace715d7df7264e17e5307eba62d8e5b Mon Sep 17 00:00:00 2001 From: i1i1 Date: Thu, 10 Aug 2023 02:02:18 +0300 Subject: [PATCH 11/20] Fix upload-keys command --- src/command/repl.rs | 7 +++---- src/command/upload_keys.rs | 36 ++++++++++++++++++------------------ 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/command/repl.rs b/src/command/repl.rs index c132cdc..5a3e25c 100644 --- a/src/command/repl.rs +++ b/src/command/repl.rs @@ -1,6 +1,6 @@ use std::io::Write; -use clap::{ArgMatches, Command as ClapCommand, FromArgMatches, Args}; +use clap::{ArgMatches, Args, Command as ClapCommand, FromArgMatches}; use tempfile::Builder as TempFileBuilder; use tokio::process::Command; @@ -12,12 +12,11 @@ use crate::nix::info::NixCheck; #[command( name = "repl", about = "Start an interactive REPL with the complete configuration", - long_about = - r#"Start an interactive REPL with the complete configuration + long_about = r#"Start an interactive REPL with the complete configuration In the REPL, you can inspect the configuration interactively with tab completion. The node configurations are accessible under the `nodes` -attribute set."#, +attribute set."# )] pub struct Opts {} diff --git a/src/command/upload_keys.rs b/src/command/upload_keys.rs index 16555d4..2ce213f 100644 --- a/src/command/upload_keys.rs +++ b/src/command/upload_keys.rs @@ -1,25 +1,25 @@ -use clap::{builder::PossibleValuesParser, Arg, Args, Command as ClapCommand}; +use clap::{Args, Command as ClapCommand}; -use crate::util; +use crate::{nix::Goal, util}; pub use super::apply::run; use super::apply::DeployOpts; -pub fn subcommand() -> ClapCommand { - let command = ClapCommand::new("upload-keys") - .about("Upload keys to remote hosts") - .long_about( - r#"Upload keys to remote hosts +#[derive(Debug, Args)] +#[command( + name = "upload-keys", + about = "Upload keys to remote hosts", + long_about = r#"Upload keys to remote hosts -This subcommand behaves as if you invoked `apply` with the pseudo `keys` goal."#, - ) - .arg( - Arg::new("goal") - .hide(true) - .default_value("keys") - .value_parser(PossibleValuesParser::new(["keys"])) - .num_args(1), - ); - - util::register_selector_args(DeployOpts::augment_args_for_update(command)) +This subcommand behaves as if you invoked `apply` with the pseudo `keys` goal."# +)] +pub struct Opts { + #[command(flatten)] + deploy: DeployOpts, + #[arg(hide = true, default_value_t = Goal::Build)] + goal: Goal, +} + +pub fn subcommand() -> ClapCommand { + Opts::augment_args(ClapCommand::new("upload-keys")) } From 1ad9301c62d63a3039f358115234ea301e33572f Mon Sep 17 00:00:00 2001 From: i1i1 Date: Thu, 10 Aug 2023 17:14:47 +0300 Subject: [PATCH 12/20] Fix panic with hive path parsing and use value parser --- src/cli.rs | 13 +++---------- src/command/nix_info.rs | 2 +- src/command/upload_keys.rs | 2 +- src/nix/hive/mod.rs | 31 +++++++++++++++++++------------ src/util.rs | 18 ------------------ 5 files changed, 24 insertions(+), 42 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index e424d93..a5d9b83 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -10,7 +10,7 @@ use clap_complete::Shell; use const_format::{concatcp, formatcp}; use env_logger::fmt::WriteStyle; -use crate::command; +use crate::{command, nix::HivePath}; /// Base URL of the manual, without the trailing slash. const MANUAL_URL_BASE: &str = "https://colmena.cli.rs"; @@ -132,15 +132,8 @@ pub fn build_cli(include_internal: bool) -> ClapCommand { .help("Path to a Hive expression, a flake.nix, or a Nix Flake URI") .long_help(Some(CONFIG_HELP)) .display_order(HELP_ORDER_FIRST) - - // The default value is a lie (sort of)! - // - // The default behavior is to search upwards from the - // current working directory for a file named "flake.nix" - // or "hive.nix". This behavior is disabled if --config/-f - // is explicitly supplied by the user (occurrences_of > 0). - .default_value("hive.nix") - .global(true)) + .global(true) + .value_parser(value_parser!(HivePath))) .arg(Arg::new("show-trace") .long("show-trace") .help("Show debug information for Nix commands") diff --git a/src/command/nix_info.rs b/src/command/nix_info.rs index 8857d5b..5a54fb8 100644 --- a/src/command/nix_info.rs +++ b/src/command/nix_info.rs @@ -1,4 +1,4 @@ -use clap::{ArgMatches, Args, FromArgMatches, Command}; +use clap::{ArgMatches, Args, Command, FromArgMatches}; use crate::error::ColmenaError; use crate::nix::evaluator::nix_eval_jobs::get_pinned_nix_eval_jobs; diff --git a/src/command/upload_keys.rs b/src/command/upload_keys.rs index 2ce213f..43f42b9 100644 --- a/src/command/upload_keys.rs +++ b/src/command/upload_keys.rs @@ -1,6 +1,6 @@ use clap::{Args, Command as ClapCommand}; -use crate::{nix::Goal, util}; +use crate::nix::Goal; pub use super::apply::run; use super::apply::DeployOpts; diff --git a/src/nix/hive/mod.rs b/src/nix/hive/mod.rs index d8a897b..2989806 100644 --- a/src/nix/hive/mod.rs +++ b/src/nix/hive/mod.rs @@ -122,20 +122,27 @@ impl FromStr for HivePath { fn from_str(s: &str) -> Result { // TODO: check for escaped colon maybe? - let path = std::path::Path::new(s); + let s = s.to_owned(); + let path = std::path::PathBuf::from(&s); + + let fut = async move { + if !path.exists() && s.contains(':') { + // Treat as flake URI + let flake = Flake::from_uri(s).await?; + + log::info!("Using flake: {}", flake.uri()); + + Ok(Self::Flake(flake)) + } else { + HivePath::from_path(path).await + } + }; + let handle = tokio::runtime::Handle::try_current() .expect("We should always be executed after we have a runtime"); - - if !path.exists() && s.contains(':') { - // Treat as flake URI - let flake = handle.block_on(Flake::from_uri(s))?; - - log::info!("Using flake: {}", flake.uri()); - - Ok(Self::Flake(flake)) - } else { - handle.block_on(HivePath::from_path(path)) - } + std::thread::spawn(move || handle.block_on(fut)) + .join() + .expect("Failed to join future") } } diff --git a/src/util.rs b/src/util.rs index 93fee77..1a07f9a 100644 --- a/src/util.rs +++ b/src/util.rs @@ -4,7 +4,6 @@ use std::error::Error; use std::process::Stdio; use async_trait::async_trait; -use clap::{Arg, Command as ClapCommand}; use futures::future::join3; use serde::de::DeserializeOwned; use tokio::io::{AsyncBufReadExt, AsyncRead, BufReader}; @@ -207,23 +206,6 @@ where Ok((s[..pos].parse()?, s[pos + 1..].parse()?)) } -pub fn register_selector_args(command: ClapCommand) -> ClapCommand { - command - .arg(Arg::new("on") - .long("on") - .value_name("NODES") - .help("Node selector") - .long_help(r#"Select a list of nodes to deploy to. - -The list is comma-separated and globs are supported. To match tags, prepend the filter by @. Valid examples: - -- host1,host2,host3 -- edge-* -- edge-*,core-* -- @a-tag,@tags-can-have-*"#) - .num_args(1)) -} - pub async fn capture_stream( mut stream: BufReader, job: Option, From 935aa77e53efb8a509ee1b8fa005bafca21937a5 Mon Sep 17 00:00:00 2001 From: i1i1 Date: Thu, 10 Aug 2023 17:57:59 +0300 Subject: [PATCH 13/20] Initial support for clap derive up to the top --- Cargo.lock | 21 ++- Cargo.toml | 4 +- src/cli.rs | 339 ++++++++++++++++++----------------- src/command/apply.rs | 22 +-- src/command/apply_local.rs | 24 +-- src/command/build.rs | 6 +- src/command/eval.rs | 32 +--- src/command/exec.rs | 26 +-- src/command/nix_info.rs | 9 +- src/command/repl.rs | 18 +- src/command/test_progress.rs | 9 +- src/command/upload_keys.rs | 6 +- src/nix/hive/mod.rs | 83 --------- src/troubleshooter.rs | 22 +-- 14 files changed, 239 insertions(+), 382 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e20f5c2..82447c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -166,9 +166,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.2.7" +version = "4.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34d21f9bf1b425d2968943631ec91202fe5e837264063503708b83013f8fc938" +checksum = "c27cdf28c0f604ba3f512b0c9a409f8de8513e4816705deb0498b627e7c3a3fd" dependencies = [ "clap_builder", "clap_derive", @@ -177,31 +177,30 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.2.7" +version = "4.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "914c8c79fb560f238ef6429439a30023c862f7a28e688c58f7203f12b29970bd" +checksum = "08a9f1ab5e9f01a9b81f202e8562eb9a10de70abf9eaeac1be465c28b75aa4aa" dependencies = [ "anstream", "anstyle", - "bitflags", "clap_lex", "strsim", ] [[package]] name = "clap_complete" -version = "4.2.3" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1594fe2312ec4abf402076e407628f5c313e54c32ade058521df4ee34ecac8a8" +checksum = "5fc443334c81a804575546c5a8a79b4913b50e28d69232903604cada1de817ce" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.2.0" +version = "4.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4" +checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" dependencies = [ "heck", "proc-macro2", @@ -211,9 +210,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" [[package]] name = "clicolors-control" diff --git a/Cargo.toml b/Cargo.toml index 7699ca2..745af53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,8 @@ edition = "2021" async-stream = "0.3.5" async-trait = "0.1.68" atty = "0.2" -clap = { version = "4.2.7", features = ["derive"] } -clap_complete = "4.2.3" +clap = { version = "4.3", features = ["derive"] } +clap_complete = "4.3" clicolors-control = "1" console = "0.15.5" const_format = "0.2.30" diff --git a/src/cli.rs b/src/cli.rs index a5d9b83..4e83b18 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -2,15 +2,16 @@ use std::env; -use clap::{ - builder::PossibleValue, value_parser, Arg, ArgAction, ArgMatches, ColorChoice, - Command as ClapCommand, ValueEnum, -}; +use clap::{CommandFactory, Parser, Subcommand, ValueEnum}; use clap_complete::Shell; use const_format::{concatcp, formatcp}; use env_logger::fmt::WriteStyle; -use crate::{command, nix::HivePath}; +use crate::{ + command, + error::ColmenaResult, + nix::{Hive, HivePath}, +}; /// Base URL of the manual, without the trailing slash. const MANUAL_URL_BASE: &str = "https://colmena.cli.rs"; @@ -66,33 +67,11 @@ const HELP_ORDER_FIRST: usize = 100; /// Display order in `--help` for arguments that are not very important. const HELP_ORDER_LOW: usize = 2000; -macro_rules! register_command { - ($module:ident, $app:ident) => { - $app = $app.subcommand(command::$module::subcommand()); - }; -} - -macro_rules! handle_command { - ($module:ident, $matches:ident) => { - if let Some(sub_matches) = $matches.subcommand_matches(stringify!($module)) { - crate::troubleshooter::run_wrapped(&$matches, &sub_matches, command::$module::run) - .await; - return; - } - }; - ($name:expr, $module:ident, $matches:ident) => { - if let Some(sub_matches) = $matches.subcommand_matches($name) { - crate::troubleshooter::run_wrapped(&$matches, &sub_matches, command::$module::run) - .await; - return; - } - }; -} - /// When to display color. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, ValueEnum)] enum ColorWhen { /// Detect automatically. + #[default] Auto, /// Always display colors. @@ -102,149 +81,199 @@ enum ColorWhen { Never, } -impl ValueEnum for ColorWhen { - fn value_variants<'a>() -> &'a [Self] { - &[Self::Auto, Self::Always, Self::Never] - } - - fn to_possible_value<'a>(&self) -> Option { - match self { - Self::Auto => Some(PossibleValue::new("auto")), - Self::Always => Some(PossibleValue::new("always")), - Self::Never => Some(PossibleValue::new("never")), - } +impl std::fmt::Display for ColorWhen { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Auto => "auto", + Self::Always => "always", + Self::Never => "never", + }) } } -pub fn build_cli(include_internal: bool) -> ClapCommand { - let version = env!("CARGO_PKG_VERSION"); - let mut app = ClapCommand::new("Colmena") - .bin_name("colmena") - .version(version) - .author("Zhaofeng Li ") - .about("NixOS deployment tool") - .long_about(LONG_ABOUT) - .arg_required_else_help(true) - .arg(Arg::new("config") - .short('f') - .long("config") - .value_name("CONFIG") - .help("Path to a Hive expression, a flake.nix, or a Nix Flake URI") - .long_help(Some(CONFIG_HELP)) - .display_order(HELP_ORDER_FIRST) - .global(true) - .value_parser(value_parser!(HivePath))) - .arg(Arg::new("show-trace") - .long("show-trace") - .help("Show debug information for Nix commands") - .long_help("Passes --show-trace to Nix commands") - .global(true) - .num_args(0)) - .arg(Arg::new("impure") - .long("impure") - .help("Allow impure expressions") - .long_help("Passes --impure to Nix commands") - .global(true) - .num_args(0)) - .arg(Arg::new("nix-option") - .long("nix-option") - .help("Passes an arbitrary option to Nix commands") - .long_help(r#"Passes arbitrary options to Nix commands +#[derive(Parser)] +#[command( + name = "Colmena", + bin_name = "colmena", + author = "Zhaofeng Li ", + version = env!("CARGO_PKG_VERSION"), + about = "NixOS deployment tool", + long_about = LONG_ABOUT, +)] +struct Opts { + #[arg( + short = 'f', + long, + value_name = "CONFIG", + help = "Path to a Hive expression, a flake.nix, or a Nix Flake URI", + long_help = CONFIG_HELP, + display_order = HELP_ORDER_FIRST, + global = true, + )] + config: Option, + #[arg( + long, + help = "Show debug information for Nix commands", + long_help = "Passes --show-trace to Nix commands", + global = true + )] + show_trace: bool, + #[arg( + long, + help = "Allow impure expressions", + long_help = "Passes --impure to Nix commands", + global = true + )] + impure: bool, + #[arg( + long, + value_parser = crate::util::parse_key_val::, + help = "Passes an arbitrary option to Nix commands", + long_help = r#"Passes arbitrary options to Nix commands This only works when building locally. -"#) - .global(true) - .num_args(2) - .value_names(["NAME", "VALUE"]) - .action(ArgAction::Append)) - .arg(Arg::new("color") - .long("color") - .help("When to colorize the output") - .long_help(r#"When to colorize the output. By default, Colmena enables colorized output when the terminal supports it. +"#, + global = true, + num_args = 2, + value_names = ["NAME, VALUE"], + )] + nix_option: Vec<(String, String)>, + #[arg( + long, + value_name = "WHEN", + default_value_t, + global = true, + display_order = HELP_ORDER_LOW, + help = "When to colorize the output", + long_help = r#"When to colorize the output. By default, Colmena enables colorized output when the terminal supports it. It's also possible to specify the preference using environment variables. See . -"#) - .display_order(HELP_ORDER_LOW) - .value_name("WHEN") - .value_parser(value_parser!(ColorWhen)) - .default_value("auto") - .global(true)); +"#, + )] + color: ColorWhen, + #[command(subcommand)] + command: Command, +} - if include_internal { - app = app.subcommand( - ClapCommand::new("gen-completions") - .about("Generate shell auto-completion files (Internal)") - .hide(true) - .arg( - Arg::new("shell") - .index(1) - .value_parser(value_parser!(Shell)) - .required(true) - .num_args(1), - ), - ); - - // TODO: handle deprecated alias - - #[cfg(debug_assertions)] - register_command!(test_progress, app); - } - - register_command!(apply, app); +#[derive(Subcommand)] +enum Command { + Apply(command::apply::Opts), #[cfg(target_os = "linux")] - register_command!(apply_local, app); - register_command!(build, app); - register_command!(eval, app); - register_command!(upload_keys, app); - register_command!(exec, app); - register_command!(repl, app); - register_command!(nix_info, app); + ApplyLocal(command::apply_local::Opts), + Build(command::build::Opts), + Eval(command::eval::Opts), + UploadKeys(command::upload_keys::Opts), + Exec(command::exec::Opts), + Repl(command::repl::Opts), + NixInfo(command::nix_info::Opts), + #[cfg(debug_assertions)] + #[command(about = "Run progress spinner tests", hide = true)] + TestProgress, + #[command(about = "Generate shell auto-completion files (Internal)", hide = true)] + GenCompletions { + shell: Shell, + }, +} - // This does _not_ take the --color flag into account (haven't - // parsed yet), only the CLICOLOR environment variable. - if clicolors_control::colors_enabled() { - app.color(ColorChoice::Always) - } else { - app +async fn get_hive(opts: &Opts) -> ColmenaResult { + let path = match &opts.config { + Some(path) => path.clone(), + None => { + // traverse upwards until we find hive.nix + let mut cur = std::env::current_dir()?; + let mut file_path = None; + + loop { + let flake = cur.join("flake.nix"); + if flake.is_file() { + file_path = Some(flake); + break; + } + + let legacy = cur.join("hive.nix"); + if legacy.is_file() { + file_path = Some(legacy); + break; + } + + match cur.parent() { + Some(parent) => { + cur = parent.to_owned(); + } + None => { + break; + } + } + } + + if file_path.is_none() { + log::error!( + "Could not find `hive.nix` or `flake.nix` in {:?} or any parent directory", + std::env::current_dir()? + ); + } + + HivePath::from_path(file_path.unwrap()).await? + } + }; + + match &path { + HivePath::Legacy(p) => { + log::info!("Using configuration: {}", p.to_string_lossy()); + } + HivePath::Flake(flake) => { + log::info!("Using flake: {}", flake.uri()); + } } + + let mut hive = Hive::new(path).await?; + + if opts.show_trace { + hive.set_show_trace(true); + } + + if opts.impure { + hive.set_impure(true); + } + + for (name, value) in opts.nix_option.iter().cloned() { + hive.add_nix_option(name, value); + } + + Ok(hive) } pub async fn run() { - let mut app = build_cli(true); - let matches = app.clone().get_matches(); + let opts = Opts::parse(); - set_color_pref(matches.get_one("color").unwrap()); + set_color_pref(&opts.color); init_logging(); - handle_command!(apply, matches); - #[cfg(target_os = "linux")] - handle_command!("apply-local", apply_local, matches); - handle_command!(build, matches); - handle_command!(eval, matches); - handle_command!("upload-keys", upload_keys, matches); - handle_command!(exec, matches); - handle_command!(repl, matches); - handle_command!("nix-info", nix_info, matches); + let hive = get_hive(&opts).await.expect("Failed to get flake or hive"); - #[cfg(debug_assertions)] - handle_command!("test-progress", test_progress, matches); + use crate::troubleshooter::run_wrapped as r; - if let Some(args) = matches.subcommand_matches("gen-completions") { - return gen_completions(args); + match opts.command { + Command::Apply(args) => r(command::apply::run(hive, args)).await, + Command::ApplyLocal(args) => r(command::apply_local::run(hive, args)).await, + Command::Eval(args) => r(command::eval::run(hive, args)).await, + Command::Exec(args) => r(command::exec::run(hive, args)).await, + Command::NixInfo(args) => r(command::nix_info::run(args)).await, + Command::Repl(args) => r(command::repl::run(hive, args)).await, + Command::TestProgress => r(command::test_progress::run()).await, + Command::Build(_args) => todo!("This is an alias for `colmena apply build`"), + Command::UploadKeys(_opts) => todo!("This is an alias for `colmena apply upload-keys`"), + Command::GenCompletions { shell } => print_completions(shell, &mut Opts::command()), } - - // deprecated alias - handle_command!("introspect", eval, matches); - - app.print_long_help().unwrap(); - println!(); } -fn gen_completions(args: &ArgMatches) { - let mut app = build_cli(false); - let shell = args.get_one::("shell").unwrap().to_owned(); - - clap_complete::generate(shell, &mut app, "colmena", &mut std::io::stdout()); +fn print_completions(shell: Shell, cmd: &mut clap::Command) { + clap_complete::generate( + shell, + cmd, + cmd.get_name().to_string(), + &mut std::io::stdout(), + ); } fn set_color_pref(when: &ColorWhen) { @@ -273,13 +302,3 @@ fn init_logging() { .write_style(style) .init(); } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_cli_debug_assert() { - build_cli(true).debug_assert() - } -} diff --git a/src/command/apply.rs b/src/command/apply.rs index b81768f..54fa256 100644 --- a/src/command/apply.rs +++ b/src/command/apply.rs @@ -1,16 +1,16 @@ use std::env; use std::path::PathBuf; -use clap::{builder::ArgPredicate, ArgMatches, Args, Command as ClapCommand, FromArgMatches}; +use clap::{builder::ArgPredicate, Args}; +use crate::error::ColmenaError; use crate::nix::{ deployment::{Deployment, EvaluationNodeLimit, EvaluatorType, Goal, Options, ParallelismLimit}, node_filter::NodeFilterOpts, + Hive, }; use crate::progress::SimpleProgressOutput; -use crate::{error::ColmenaError, nix::hive::HiveArgs}; - #[derive(Debug, Args)] pub struct DeployOpts { #[arg( @@ -117,7 +117,7 @@ This is an experimental feature."# #[derive(Debug, Args)] #[command(name = "apply", about = "Apply configurations on remote machines")] -struct Opts { +pub struct Opts { #[arg( help = "Deployment goal", long_help = r#"The goal of the deployment. @@ -139,17 +139,7 @@ Same as the targets for switch-to-configuration, with the following extra pseudo node_filter: NodeFilterOpts, } -pub fn subcommand() -> ClapCommand { - Opts::augment_args(ClapCommand::new("apply")) -} - -pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(), ColmenaError> { - let hive = HiveArgs::from_arg_matches(local_args) - .unwrap() - .into_hive() - .await - .unwrap(); - +pub async fn run(hive: Hive, opts: Opts) -> Result<(), ColmenaError> { let ssh_config = env::var("SSH_CONFIG_FILE").ok().map(PathBuf::from); let Opts { @@ -170,7 +160,7 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<( evaluator, }, node_filter, - } = Opts::from_arg_matches(local_args).expect("Failed to parse `apply` args"); + } = opts; if node_filter.on.is_none() && goal != Goal::Build { // User did not specify node, we should check meta and see rules diff --git a/src/command/apply_local.rs b/src/command/apply_local.rs index 29e5df9..9ec38dc 100644 --- a/src/command/apply_local.rs +++ b/src/command/apply_local.rs @@ -1,13 +1,13 @@ use regex::Regex; use std::collections::HashMap; -use clap::{ArgMatches, Args, Command as ClapCommand, FromArgMatches}; +use clap::Args; use tokio::fs; use crate::error::ColmenaError; use crate::nix::deployment::{Deployment, Goal, Options, TargetNode}; -use crate::nix::hive::HiveArgs; +use crate::nix::Hive; use crate::nix::{host::Local as LocalHost, NodeName}; use crate::progress::SimpleProgressOutput; @@ -51,25 +51,19 @@ By default, Colmena will deploy keys set in `deployment.keys` before activating help = "Removed: Configure deployment.privilegeEscalationCommand in node configuration" )] sudo_command: Option, - #[command(flatten)] - hive_args: HiveArgs, } -pub fn subcommand() -> ClapCommand { - Opts::augment_args(ClapCommand::new("apply-local")) -} - -pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(), ColmenaError> { - let Opts { +pub async fn run( + hive: Hive, + Opts { goal, sudo, verbose, no_keys, node, sudo_command, - hive_args, - } = Opts::from_arg_matches(local_args).expect("Failed to parse `apply-local` options."); - + }: Opts, +) -> Result<(), ColmenaError> { if sudo_command.is_some() { log::error!("--sudo-command has been removed. Please configure it in deployment.privilegeEscalationCommand in the node configuration."); quit::with_code(1); @@ -97,10 +91,6 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<( } } - let hive = hive_args - .into_hive() - .await - .expect("Failed to get hive from arguments"); let hostname = NodeName::new(node.unwrap_or_else(|| { hostname::get() .expect("Could not get hostname") diff --git a/src/command/build.rs b/src/command/build.rs index 170c435..d8682a2 100644 --- a/src/command/build.rs +++ b/src/command/build.rs @@ -1,4 +1,4 @@ -use clap::{Args, Command as ClapCommand}; +use clap::Args; use crate::nix::Goal; @@ -19,7 +19,3 @@ pub struct Opts { #[arg(hide = true, default_value_t = Goal::Build)] goal: Goal, } - -pub fn subcommand() -> ClapCommand { - Opts::augment_args(ClapCommand::new("build")) -} diff --git a/src/command/eval.rs b/src/command/eval.rs index b9bfa5d..e5d42c5 100644 --- a/src/command/eval.rs +++ b/src/command/eval.rs @@ -1,9 +1,9 @@ use std::path::PathBuf; -use clap::{ArgMatches, Args, Command as ClapCommand, FromArgMatches}; +use clap::Args; use crate::error::ColmenaError; -use crate::nix::hive::HiveArgs; +use crate::nix::Hive; #[derive(Debug, Args)] #[command( @@ -32,29 +32,15 @@ pub struct Opts { expression_file: Option, } -pub fn subcommand() -> ClapCommand { - Opts::augment_args(ClapCommand::new("eval")) -} - -pub async fn run(global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(), ColmenaError> { - if let Some("introspect") = global_args.subcommand_name() { - log::warn!( - "`colmena introspect` has been renamed to `colmena eval`. Please update your scripts." - ); - } - - let hive = HiveArgs::from_arg_matches(local_args) - .unwrap() - .into_hive() - .await - .unwrap(); - - let Opts { - instantiate, +pub async fn run( + hive: Hive, + Opts { expression, + instantiate, expression_file, - } = Opts::from_arg_matches(local_args).expect("Failed to parse args"); - + }: Opts, +) -> Result<(), ColmenaError> { + // TODO: check for deprecated alias let expression = expression_file .map(|path| { format!( diff --git a/src/command/exec.rs b/src/command/exec.rs index 6410966..a2b8d38 100644 --- a/src/command/exec.rs +++ b/src/command/exec.rs @@ -2,20 +2,20 @@ use std::env; use std::path::PathBuf; use std::sync::Arc; -use clap::{ArgMatches, Args, Command as ClapCommand, FromArgMatches}; +use clap::Args; use futures::future::join_all; use tokio::sync::Semaphore; use crate::error::ColmenaError; use crate::job::{JobMonitor, JobState, JobType}; -use crate::nix::hive::HiveArgs; use crate::nix::node_filter::NodeFilterOpts; +use crate::nix::Hive; use crate::progress::SimpleProgressOutput; use crate::util; #[derive(Debug, Args)] #[command(name = "exec", about = "Run a command on remote machines")] -struct Opts { +pub struct Opts { #[arg( short, long, @@ -52,24 +52,16 @@ It's recommended to use -- to separate Colmena options from the command to run. command: Vec, } -pub fn subcommand() -> ClapCommand { - Opts::augment_args(ClapCommand::new("exec")) -} - -pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(), ColmenaError> { - let hive = HiveArgs::from_arg_matches(local_args) - .unwrap() - .into_hive() - .await - .unwrap(); - let ssh_config = env::var("SSH_CONFIG_FILE").ok().map(PathBuf::from); - - let Opts { +pub async fn run( + hive: Hive, + Opts { parallel, verbose, nodes, command, - } = Opts::from_arg_matches(local_args).unwrap(); + }: Opts, +) -> Result<(), ColmenaError> { + let ssh_config = env::var("SSH_CONFIG_FILE").ok().map(PathBuf::from); let mut targets = hive.select_nodes(nodes.on, ssh_config, true).await?; diff --git a/src/command/nix_info.rs b/src/command/nix_info.rs index 5a54fb8..ea0b240 100644 --- a/src/command/nix_info.rs +++ b/src/command/nix_info.rs @@ -1,4 +1,4 @@ -use clap::{ArgMatches, Args, Command, FromArgMatches}; +use clap::Args; use crate::error::ColmenaError; use crate::nix::evaluator::nix_eval_jobs::get_pinned_nix_eval_jobs; @@ -11,12 +11,7 @@ use crate::nix::NixCheck; )] pub struct Opts {} -pub fn subcommand() -> Command { - Opts::augment_args(Command::new("nix-info")) -} - -pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(), ColmenaError> { - let Opts {} = Opts::from_arg_matches(local_args).unwrap(); +pub async fn run(_: Opts) -> Result<(), ColmenaError> { let check = NixCheck::detect().await; check.print_version_info(); check.print_flakes_info(false); diff --git a/src/command/repl.rs b/src/command/repl.rs index 5a3e25c..a9240d7 100644 --- a/src/command/repl.rs +++ b/src/command/repl.rs @@ -1,12 +1,12 @@ use std::io::Write; -use clap::{ArgMatches, Args, Command as ClapCommand, FromArgMatches}; +use clap::Args; use tempfile::Builder as TempFileBuilder; use tokio::process::Command; use crate::error::ColmenaError; -use crate::nix::hive::HiveArgs; use crate::nix::info::NixCheck; +use crate::nix::Hive; #[derive(Debug, Args)] #[command( @@ -20,22 +20,10 @@ attribute set."# )] pub struct Opts {} -pub fn subcommand() -> ClapCommand { - Opts::augment_args(ClapCommand::new("repl")) -} - -pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(), ColmenaError> { - let Opts {} = Opts::from_arg_matches(local_args).unwrap(); - +pub async fn run(hive: Hive, _: Opts) -> Result<(), ColmenaError> { let nix_check = NixCheck::detect().await; let nix_version = nix_check.version().expect("Could not detect Nix version"); - let hive = HiveArgs::from_arg_matches(local_args) - .unwrap() - .into_hive() - .await - .unwrap(); - let expr = hive.get_repl_expression(); let mut expr_file = TempFileBuilder::new() diff --git a/src/command/test_progress.rs b/src/command/test_progress.rs index df21980..9cba556 100644 --- a/src/command/test_progress.rs +++ b/src/command/test_progress.rs @@ -1,6 +1,5 @@ use std::time::Duration; -use clap::{ArgMatches, Command as ClapCommand}; use tokio::time; use crate::error::{ColmenaError, ColmenaResult}; @@ -14,13 +13,7 @@ macro_rules! node { }; } -pub fn subcommand() -> ClapCommand { - ClapCommand::new("test-progress") - .about("Run progress spinner tests") - .hide(true) -} - -pub async fn run(_global_args: &ArgMatches, _local_args: &ArgMatches) -> Result<(), ColmenaError> { +pub async fn run() -> Result<(), ColmenaError> { let mut output = SpinnerOutput::new(); let (monitor, meta) = JobMonitor::new(output.get_sender()); diff --git a/src/command/upload_keys.rs b/src/command/upload_keys.rs index 43f42b9..9cc6e3e 100644 --- a/src/command/upload_keys.rs +++ b/src/command/upload_keys.rs @@ -1,4 +1,4 @@ -use clap::{Args, Command as ClapCommand}; +use clap::Args; use crate::nix::Goal; @@ -19,7 +19,3 @@ pub struct Opts { #[arg(hide = true, default_value_t = Goal::Build)] goal: Goal, } - -pub fn subcommand() -> ClapCommand { - Opts::augment_args(ClapCommand::new("upload-keys")) -} diff --git a/src/nix/hive/mod.rs b/src/nix/hive/mod.rs index 2989806..f9d9227 100644 --- a/src/nix/hive/mod.rs +++ b/src/nix/hive/mod.rs @@ -8,7 +8,6 @@ use std::convert::AsRef; use std::path::{Path, PathBuf}; use std::str::FromStr; -use clap::Args; use tokio::process::Command; use tokio::sync::OnceCell; use validator::Validate; @@ -23,88 +22,6 @@ use crate::job::JobHandle; use crate::util::{CommandExecution, CommandExt}; use assets::Assets; -#[derive(Debug, Args)] -pub struct HiveArgs { - #[arg(short = 'f', long, value_name = "CONFIG")] - config: Option, - #[arg(long)] - show_trace: bool, - #[arg(long)] - impure: bool, - #[arg(long, value_parser = crate::util::parse_key_val::)] - nix_option: Vec<(String, String)>, -} - -impl HiveArgs { - pub async fn into_hive(self) -> ColmenaResult { - let path = match self.config { - Some(path) => path, - None => { - // traverse upwards until we find hive.nix - let mut cur = std::env::current_dir()?; - let mut file_path = None; - - loop { - let flake = cur.join("flake.nix"); - if flake.is_file() { - file_path = Some(flake); - break; - } - - let legacy = cur.join("hive.nix"); - if legacy.is_file() { - file_path = Some(legacy); - break; - } - - match cur.parent() { - Some(parent) => { - cur = parent.to_owned(); - } - None => { - break; - } - } - } - - if file_path.is_none() { - log::error!( - "Could not find `hive.nix` or `flake.nix` in {:?} or any parent directory", - std::env::current_dir()? - ); - } - - HivePath::from_path(file_path.unwrap()).await? - } - }; - - match &path { - HivePath::Legacy(p) => { - log::info!("Using configuration: {}", p.to_string_lossy()); - } - HivePath::Flake(flake) => { - log::info!("Using flake: {}", flake.uri()); - } - } - - let mut hive = Hive::new(path).await?; - - if self.show_trace { - hive.set_show_trace(true); - } - - if self.impure { - hive.set_impure(true); - } - - for (name, value) in self.nix_option { - hive.add_nix_option(name, value); - } - - Ok(hive) - } -} - #[derive(Debug, Clone)] pub enum HivePath { /// A Nix Flake. diff --git a/src/troubleshooter.rs b/src/troubleshooter.rs index f185421..58a2ec3 100644 --- a/src/troubleshooter.rs +++ b/src/troubleshooter.rs @@ -11,27 +11,23 @@ use snafu::ErrorCompat; use crate::error::ColmenaError; /// Runs a closure and tries to troubleshoot if it returns an error. -pub async fn run_wrapped<'a, F, U, T>( - global_args: &'a ArgMatches, - local_args: &'a ArgMatches, - f: U, -) -> T +pub async fn run_wrapped<'a, F, T>(f: F) -> T where - U: FnOnce(&'a ArgMatches, &'a ArgMatches) -> F, F: Future>, { - match f(global_args, local_args).await { + match f.await { Ok(r) => r, Err(error) => { log::error!("-----"); log::error!("Operation failed with error: {}", error); - if let Err(own_error) = troubleshoot(global_args, local_args, &error) { - log::error!( - "Error occurred while trying to troubleshoot another error: {}", - own_error - ); - } + // TODO: support troubleshooting + // if let Err(own_error) = troubleshoot(hive, &error) { + // log::error!( + // "Error occurred while trying to troubleshoot another error: {}", + // own_error + // ); + // } // Ensure we exit with a code quit::with_code(1); From 765a5d2ef34abf771d41b5166dc6a25db80cf93c Mon Sep 17 00:00:00 2001 From: i1i1 Date: Thu, 10 Aug 2023 18:06:08 +0300 Subject: [PATCH 14/20] Fix build and upload-keys --- src/cli.rs | 42 +++++++++++++++++++++++++++++++++----- src/command/apply.rs | 6 +++--- src/command/build.rs | 21 ------------------- src/command/mod.rs | 2 -- src/command/upload_keys.rs | 21 ------------------- src/nix/node_filter.rs | 2 +- 6 files changed, 41 insertions(+), 53 deletions(-) delete mode 100644 src/command/build.rs delete mode 100644 src/command/upload_keys.rs diff --git a/src/cli.rs b/src/cli.rs index 4e83b18..0d5b15c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -8,7 +8,7 @@ use const_format::{concatcp, formatcp}; use env_logger::fmt::WriteStyle; use crate::{ - command, + command::{self, apply::DeployOpts}, error::ColmenaResult, nix::{Hive, HivePath}, }; @@ -160,9 +160,27 @@ enum Command { Apply(command::apply::Opts), #[cfg(target_os = "linux")] ApplyLocal(command::apply_local::Opts), - Build(command::build::Opts), + #[command( + about = "Build configurations but not push to remote machines", + long_about = r#"Build configurations but not push to remote machines + +This subcommand behaves as if you invoked `apply` with the `build` goal."# + )] + Build { + #[command(flatten)] + deploy: DeployOpts, + }, Eval(command::eval::Opts), - UploadKeys(command::upload_keys::Opts), + #[command( + about = "Upload keys to remote hosts", + long_about = r#"Upload keys to remote hosts + +This subcommand behaves as if you invoked `apply` with the pseudo `keys` goal."# + )] + UploadKeys { + #[command(flatten)] + deploy: DeployOpts, + }, Exec(command::exec::Opts), Repl(command::repl::Opts), NixInfo(command::nix_info::Opts), @@ -261,8 +279,22 @@ pub async fn run() { Command::NixInfo(args) => r(command::nix_info::run(args)).await, Command::Repl(args) => r(command::repl::run(hive, args)).await, Command::TestProgress => r(command::test_progress::run()).await, - Command::Build(_args) => todo!("This is an alias for `colmena apply build`"), - Command::UploadKeys(_opts) => todo!("This is an alias for `colmena apply upload-keys`"), + Command::Build { deploy } => { + let args = command::apply::Opts { + deploy, + goal: crate::nix::Goal::Build, + node_filter: Default::default(), + }; + r(command::apply::run(hive, args)).await + }, + Command::UploadKeys { deploy } => { + let args = command::apply::Opts { + deploy, + goal: crate::nix::Goal::UploadKeys, + node_filter: Default::default(), + }; + r(command::apply::run(hive, args)).await + }, Command::GenCompletions { shell } => print_completions(shell, &mut Opts::command()), } } diff --git a/src/command/apply.rs b/src/command/apply.rs index 54fa256..823d3a6 100644 --- a/src/command/apply.rs +++ b/src/command/apply.rs @@ -132,11 +132,11 @@ Same as the targets for switch-to-configuration, with the following extra pseudo "#, default_value_if("reboot", ArgPredicate::IsPresent, Some("boot")) )] - goal: Goal, + pub goal: Goal, #[command(flatten)] - deploy: DeployOpts, + pub deploy: DeployOpts, #[command(flatten)] - node_filter: NodeFilterOpts, + pub node_filter: NodeFilterOpts, } pub async fn run(hive: Hive, opts: Opts) -> Result<(), ColmenaError> { diff --git a/src/command/build.rs b/src/command/build.rs deleted file mode 100644 index d8682a2..0000000 --- a/src/command/build.rs +++ /dev/null @@ -1,21 +0,0 @@ -use clap::Args; - -use crate::nix::Goal; - -pub use super::apply::run; -use super::apply::DeployOpts; - -#[derive(Debug, Args)] -#[command( - name = "build", - about = "Build configurations but not push to remote machines", - long_about = r#"Build configurations but not push to remote machines - -This subcommand behaves as if you invoked `apply` with the `build` goal."# -)] -pub struct Opts { - #[command(flatten)] - deploy: DeployOpts, - #[arg(hide = true, default_value_t = Goal::Build)] - goal: Goal, -} diff --git a/src/command/mod.rs b/src/command/mod.rs index d982081..cc6c8cb 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -1,10 +1,8 @@ pub mod apply; -pub mod build; pub mod eval; pub mod exec; pub mod nix_info; pub mod repl; -pub mod upload_keys; #[cfg(target_os = "linux")] pub mod apply_local; diff --git a/src/command/upload_keys.rs b/src/command/upload_keys.rs deleted file mode 100644 index 9cc6e3e..0000000 --- a/src/command/upload_keys.rs +++ /dev/null @@ -1,21 +0,0 @@ -use clap::Args; - -use crate::nix::Goal; - -pub use super::apply::run; -use super::apply::DeployOpts; - -#[derive(Debug, Args)] -#[command( - name = "upload-keys", - about = "Upload keys to remote hosts", - long_about = r#"Upload keys to remote hosts - -This subcommand behaves as if you invoked `apply` with the pseudo `keys` goal."# -)] -pub struct Opts { - #[command(flatten)] - deploy: DeployOpts, - #[arg(hide = true, default_value_t = Goal::Build)] - goal: Goal, -} diff --git a/src/nix/node_filter.rs b/src/nix/node_filter.rs index f018cd8..0e99801 100644 --- a/src/nix/node_filter.rs +++ b/src/nix/node_filter.rs @@ -10,7 +10,7 @@ use glob::Pattern as GlobPattern; use super::{ColmenaError, ColmenaResult, NodeConfig, NodeName}; -#[derive(Debug, Args)] +#[derive(Debug, Default, Args)] pub struct NodeFilterOpts { #[arg( long, From c79a872438f3241d041cf46eab1bc85d5164eec5 Mon Sep 17 00:00:00 2001 From: i1i1 Date: Fri, 11 Aug 2023 15:36:27 +0300 Subject: [PATCH 15/20] Enable troubleshooting --- src/cli.rs | 22 +++++++++++----------- src/troubleshooter.rs | 31 +++++++++++-------------------- 2 files changed, 22 insertions(+), 31 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 0d5b15c..e9d74a5 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -272,29 +272,29 @@ pub async fn run() { use crate::troubleshooter::run_wrapped as r; match opts.command { - Command::Apply(args) => r(command::apply::run(hive, args)).await, - Command::ApplyLocal(args) => r(command::apply_local::run(hive, args)).await, - Command::Eval(args) => r(command::eval::run(hive, args)).await, - Command::Exec(args) => r(command::exec::run(hive, args)).await, - Command::NixInfo(args) => r(command::nix_info::run(args)).await, - Command::Repl(args) => r(command::repl::run(hive, args)).await, - Command::TestProgress => r(command::test_progress::run()).await, + Command::Apply(args) => r(command::apply::run(hive, args), opts.config).await, + Command::ApplyLocal(args) => r(command::apply_local::run(hive, args), opts.config).await, + Command::Eval(args) => r(command::eval::run(hive, args), opts.config).await, + Command::Exec(args) => r(command::exec::run(hive, args), opts.config).await, + Command::NixInfo(args) => r(command::nix_info::run(args), opts.config).await, + Command::Repl(args) => r(command::repl::run(hive, args), opts.config).await, + Command::TestProgress => r(command::test_progress::run(), opts.config).await, Command::Build { deploy } => { let args = command::apply::Opts { deploy, goal: crate::nix::Goal::Build, node_filter: Default::default(), }; - r(command::apply::run(hive, args)).await - }, + r(command::apply::run(hive, args), opts.config).await + } Command::UploadKeys { deploy } => { let args = command::apply::Opts { deploy, goal: crate::nix::Goal::UploadKeys, node_filter: Default::default(), }; - r(command::apply::run(hive, args)).await - }, + r(command::apply::run(hive, args), opts.config).await + } Command::GenCompletions { shell } => print_completions(shell, &mut Opts::command()), } } diff --git a/src/troubleshooter.rs b/src/troubleshooter.rs index 58a2ec3..06ec7d6 100644 --- a/src/troubleshooter.rs +++ b/src/troubleshooter.rs @@ -5,13 +5,12 @@ use std::env; use std::future::Future; -use clap::{parser::ValueSource as ClapValueSource, ArgMatches}; use snafu::ErrorCompat; -use crate::error::ColmenaError; +use crate::{error::ColmenaError, nix::HivePath}; /// Runs a closure and tries to troubleshoot if it returns an error. -pub async fn run_wrapped<'a, F, T>(f: F) -> T +pub async fn run_wrapped<'a, F, T>(f: F, hive_config: Option) -> T where F: Future>, { @@ -21,13 +20,12 @@ where log::error!("-----"); log::error!("Operation failed with error: {}", error); - // TODO: support troubleshooting - // if let Err(own_error) = troubleshoot(hive, &error) { - // log::error!( - // "Error occurred while trying to troubleshoot another error: {}", - // own_error - // ); - // } + if let Err(own_error) = troubleshoot(hive_config, &error) { + log::error!( + "Error occurred while trying to troubleshoot another error: {}", + own_error + ); + } // Ensure we exit with a code quit::with_code(1); @@ -35,17 +33,13 @@ where } } -fn troubleshoot( - global_args: &ArgMatches, - _local_args: &ArgMatches, - error: &ColmenaError, -) -> Result<(), ColmenaError> { +fn troubleshoot(hive_config: Option, error: &ColmenaError) -> Result<(), ColmenaError> { if let ColmenaError::NoFlakesSupport = error { // People following the tutorial might put hive.nix directly // in their Colmena checkout, and encounter NoFlakesSupport // because Colmena always prefers flake.nix when it exists. - if let Some(ClapValueSource::DefaultValue) = global_args.value_source("config") { + if hive_config.is_none() { let cwd = env::current_dir()?; if cwd.join("flake.nix").is_file() && cwd.join("hive.nix").is_file() { eprintln!( @@ -71,8 +65,5 @@ fn troubleshoot( } fn backtrace_enabled() -> bool { - match env::var("RUST_BACKTRACE") { - Ok(backtrace_conf) => backtrace_conf != "0", - _ => false, - } + matches!(env::var("RUST_BACKTRACE"), Ok(backtrace_conf) if backtrace_conf != "0") } From 303cf14fc6b48f3733bc29f0f7614bc12db6a836 Mon Sep 17 00:00:00 2001 From: i1i1 Date: Fri, 11 Aug 2023 15:41:41 +0300 Subject: [PATCH 16/20] Remove empty options for subcommands --- src/cli.rs | 17 +++++++++++++---- src/command/nix_info.rs | 13 ++----------- src/command/repl.rs | 17 ++--------------- 3 files changed, 17 insertions(+), 30 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index e9d74a5..aec55e5 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -182,8 +182,17 @@ This subcommand behaves as if you invoked `apply` with the pseudo `keys` goal."# deploy: DeployOpts, }, Exec(command::exec::Opts), - Repl(command::repl::Opts), - NixInfo(command::nix_info::Opts), + #[command( + about = "Start an interactive REPL with the complete configuration", + long_about = r#"Start an interactive REPL with the complete configuration + +In the REPL, you can inspect the configuration interactively with tab +completion. The node configurations are accessible under the `nodes` +attribute set."# + )] + Repl, + #[command(about = "Show information about the current Nix installation")] + NixInfo, #[cfg(debug_assertions)] #[command(about = "Run progress spinner tests", hide = true)] TestProgress, @@ -276,8 +285,8 @@ pub async fn run() { Command::ApplyLocal(args) => r(command::apply_local::run(hive, args), opts.config).await, Command::Eval(args) => r(command::eval::run(hive, args), opts.config).await, Command::Exec(args) => r(command::exec::run(hive, args), opts.config).await, - Command::NixInfo(args) => r(command::nix_info::run(args), opts.config).await, - Command::Repl(args) => r(command::repl::run(hive, args), opts.config).await, + Command::NixInfo => r(command::nix_info::run(), opts.config).await, + Command::Repl => r(command::repl::run(hive), opts.config).await, Command::TestProgress => r(command::test_progress::run(), opts.config).await, Command::Build { deploy } => { let args = command::apply::Opts { diff --git a/src/command/nix_info.rs b/src/command/nix_info.rs index ea0b240..c63c9f2 100644 --- a/src/command/nix_info.rs +++ b/src/command/nix_info.rs @@ -1,17 +1,8 @@ -use clap::Args; - -use crate::error::ColmenaError; +use crate::error::ColmenaResult; use crate::nix::evaluator::nix_eval_jobs::get_pinned_nix_eval_jobs; use crate::nix::NixCheck; -#[derive(Debug, Args)] -#[command( - name = "nix-info", - about = "Show information about the current Nix installation" -)] -pub struct Opts {} - -pub async fn run(_: Opts) -> Result<(), ColmenaError> { +pub async fn run() -> ColmenaResult<()> { let check = NixCheck::detect().await; check.print_version_info(); check.print_flakes_info(false); diff --git a/src/command/repl.rs b/src/command/repl.rs index a9240d7..28e6e45 100644 --- a/src/command/repl.rs +++ b/src/command/repl.rs @@ -1,26 +1,13 @@ use std::io::Write; -use clap::Args; use tempfile::Builder as TempFileBuilder; use tokio::process::Command; -use crate::error::ColmenaError; +use crate::error::ColmenaResult; use crate::nix::info::NixCheck; use crate::nix::Hive; -#[derive(Debug, Args)] -#[command( - name = "repl", - about = "Start an interactive REPL with the complete configuration", - long_about = r#"Start an interactive REPL with the complete configuration - -In the REPL, you can inspect the configuration interactively with tab -completion. The node configurations are accessible under the `nodes` -attribute set."# -)] -pub struct Opts {} - -pub async fn run(hive: Hive, _: Opts) -> Result<(), ColmenaError> { +pub async fn run(hive: Hive) -> ColmenaResult<()> { let nix_check = NixCheck::detect().await; let nix_version = nix_check.version().expect("Could not detect Nix version"); From 5e17c629b290d40525f2d3a6b2090d243185ede8 Mon Sep 17 00:00:00 2001 From: i1i1 Date: Fri, 11 Aug 2023 15:53:35 +0300 Subject: [PATCH 17/20] Remove todo for deprecated alias (aliases are hiddin by default) --- src/command/eval.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/command/eval.rs b/src/command/eval.rs index e5d42c5..3a7259f 100644 --- a/src/command/eval.rs +++ b/src/command/eval.rs @@ -40,7 +40,6 @@ pub async fn run( expression_file, }: Opts, ) -> Result<(), ColmenaError> { - // TODO: check for deprecated alias let expression = expression_file .map(|path| { format!( From fc5c6d4544c037adddb061e1e0fc2a9faf28dc1b Mon Sep 17 00:00:00 2001 From: i1i1 Date: Fri, 11 Aug 2023 18:17:16 +0300 Subject: [PATCH 18/20] Fix `--nix-option` flag --- src/cli.rs | 12 +++++++----- src/util.rs | 15 --------------- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index aec55e5..ebfebcd 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -127,7 +127,6 @@ struct Opts { impure: bool, #[arg( long, - value_parser = crate::util::parse_key_val::, help = "Passes an arbitrary option to Nix commands", long_help = r#"Passes arbitrary options to Nix commands @@ -135,9 +134,9 @@ This only works when building locally. "#, global = true, num_args = 2, - value_names = ["NAME, VALUE"], + value_names = ["NAME", "VALUE"], )] - nix_option: Vec<(String, String)>, + nix_option: Vec, #[arg( long, value_name = "WHEN", @@ -263,8 +262,11 @@ async fn get_hive(opts: &Opts) -> ColmenaResult { hive.set_impure(true); } - for (name, value) in opts.nix_option.iter().cloned() { - hive.add_nix_option(name, value); + for chunks in opts.nix_option.chunks_exact(2) { + let [name, value] = chunks else { + unreachable!() + }; + hive.add_nix_option(name.clone(), value.clone()); } Ok(hive) diff --git a/src/util.rs b/src/util.rs index 1a07f9a..ffbfef5 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,5 +1,4 @@ use std::convert::TryFrom; -use std::error::Error; use std::process::Stdio; @@ -192,20 +191,6 @@ impl CommandExt for CommandExecution { } } -/// Parse a single key-value pair -pub fn parse_key_val(s: &str) -> Result<(T, U), Box> -where - T: std::str::FromStr, - T::Err: Error + Send + Sync + 'static, - U: std::str::FromStr, - U::Err: Error + Send + Sync + 'static, -{ - let pos = s - .find('=') - .ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?; - Ok((s[..pos].parse()?, s[pos + 1..].parse()?)) -} - pub async fn capture_stream( mut stream: BufReader, job: Option, From 495fc56f6effc7e5e7e31f714dbf2924605e1271 Mon Sep 17 00:00:00 2001 From: i1i1 Date: Fri, 11 Aug 2023 18:33:45 +0300 Subject: [PATCH 19/20] Remove some commands on some profiles and add default goal for apply --- src/cli.rs | 2 ++ src/command/apply.rs | 1 + 2 files changed, 3 insertions(+) diff --git a/src/cli.rs b/src/cli.rs index ebfebcd..149583d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -284,11 +284,13 @@ pub async fn run() { match opts.command { Command::Apply(args) => r(command::apply::run(hive, args), opts.config).await, + #[cfg(target_os = "linux")] Command::ApplyLocal(args) => r(command::apply_local::run(hive, args), opts.config).await, Command::Eval(args) => r(command::eval::run(hive, args), opts.config).await, Command::Exec(args) => r(command::exec::run(hive, args), opts.config).await, Command::NixInfo => r(command::nix_info::run(), opts.config).await, Command::Repl => r(command::repl::run(hive), opts.config).await, + #[cfg(debug_assertions)] Command::TestProgress => r(command::test_progress::run(), opts.config).await, Command::Build { deploy } => { let args = command::apply::Opts { diff --git a/src/command/apply.rs b/src/command/apply.rs index 823d3a6..db3e791 100644 --- a/src/command/apply.rs +++ b/src/command/apply.rs @@ -130,6 +130,7 @@ Same as the targets for switch-to-configuration, with the following extra pseudo `switch` is the default goal unless `--reboot` is passed, in which case `boot` is the default. "#, + default_value_t, default_value_if("reboot", ArgPredicate::IsPresent, Some("boot")) )] pub goal: Goal, From 95ded7fddbb37a62b2d337862e641cebdd428e88 Mon Sep 17 00:00:00 2001 From: i1i1 Date: Fri, 11 Aug 2023 19:04:39 +0300 Subject: [PATCH 20/20] Add node filter to `build` and `upload-keys` --- src/cli.rs | 2 -- src/command/apply.rs | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 149583d..bd33c2d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -296,7 +296,6 @@ pub async fn run() { let args = command::apply::Opts { deploy, goal: crate::nix::Goal::Build, - node_filter: Default::default(), }; r(command::apply::run(hive, args), opts.config).await } @@ -304,7 +303,6 @@ pub async fn run() { let args = command::apply::Opts { deploy, goal: crate::nix::Goal::UploadKeys, - node_filter: Default::default(), }; r(command::apply::run(hive, args), opts.config).await } diff --git a/src/command/apply.rs b/src/command/apply.rs index db3e791..33395db 100644 --- a/src/command/apply.rs +++ b/src/command/apply.rs @@ -113,6 +113,8 @@ will treat deployment.replaceUnknownProfiles as though it was set true and perfo This is an experimental feature."# )] evaluator: EvaluatorType, + #[command(flatten)] + node_filter: NodeFilterOpts, } #[derive(Debug, Args)] @@ -136,8 +138,6 @@ Same as the targets for switch-to-configuration, with the following extra pseudo pub goal: Goal, #[command(flatten)] pub deploy: DeployOpts, - #[command(flatten)] - pub node_filter: NodeFilterOpts, } pub async fn run(hive: Hive, opts: Opts) -> Result<(), ColmenaError> { @@ -159,8 +159,8 @@ pub async fn run(hive: Hive, opts: Opts) -> Result<(), ColmenaError> { no_build_on_target, force_replace_unknown_profiles, evaluator, + node_filter, }, - node_filter, } = opts; if node_filter.on.is_none() && goal != Goal::Build {