Merge pull request #169 from i1i1/clap-derive

Switch to clap derive
This commit is contained in:
Zhaofeng Li 2023-10-24 12:51:57 +02:00 committed by GitHub
commit 3538f18b30
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 696 additions and 804 deletions

33
Cargo.lock generated
View file

@ -166,40 +166,53 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.2.7" version = "4.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34d21f9bf1b425d2968943631ec91202fe5e837264063503708b83013f8fc938" checksum = "c27cdf28c0f604ba3f512b0c9a409f8de8513e4816705deb0498b627e7c3a3fd"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive",
"once_cell",
] ]
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.2.7" version = "4.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "914c8c79fb560f238ef6429439a30023c862f7a28e688c58f7203f12b29970bd" checksum = "08a9f1ab5e9f01a9b81f202e8562eb9a10de70abf9eaeac1be465c28b75aa4aa"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
"bitflags",
"clap_lex", "clap_lex",
"strsim", "strsim",
] ]
[[package]] [[package]]
name = "clap_complete" name = "clap_complete"
version = "4.2.3" version = "4.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1594fe2312ec4abf402076e407628f5c313e54c32ade058521df4ee34ecac8a8" checksum = "5fc443334c81a804575546c5a8a79b4913b50e28d69232903604cada1de817ce"
dependencies = [ dependencies = [
"clap", "clap",
] ]
[[package]] [[package]]
name = "clap_lex" name = "clap_derive"
version = "0.4.1" version = "4.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.16",
]
[[package]]
name = "clap_lex"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
[[package]] [[package]]
name = "clicolors-control" name = "clicolors-control"

View file

@ -10,8 +10,8 @@ edition = "2021"
async-stream = "0.3.5" async-stream = "0.3.5"
async-trait = "0.1.68" async-trait = "0.1.68"
atty = "0.2" atty = "0.2"
clap = "4.2.7" clap = { version = "4.3", features = ["derive"] }
clap_complete = "4.2.3" clap_complete = "4.3"
clicolors-control = "1" clicolors-control = "1"
console = "0.15.5" console = "0.15.5"
const_format = "0.2.30" const_format = "0.2.30"

View file

@ -2,15 +2,16 @@
use std::env; use std::env;
use clap::{ use clap::{CommandFactory, Parser, Subcommand, ValueEnum};
builder::PossibleValue, value_parser, Arg, ArgAction, ArgMatches, ColorChoice,
Command as ClapCommand, ValueEnum,
};
use clap_complete::Shell; use clap_complete::Shell;
use const_format::{concatcp, formatcp}; use const_format::{concatcp, formatcp};
use env_logger::fmt::WriteStyle; use env_logger::fmt::WriteStyle;
use crate::command; use crate::{
command::{self, apply::DeployOpts},
error::ColmenaResult,
nix::{Hive, HivePath},
};
/// Base URL of the manual, without the trailing slash. /// Base URL of the manual, without the trailing slash.
const MANUAL_URL_BASE: &str = "https://colmena.cli.rs"; 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. /// Display order in `--help` for arguments that are not very important.
const HELP_ORDER_LOW: usize = 2000; 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. /// When to display color.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, ValueEnum)]
enum ColorWhen { enum ColorWhen {
/// Detect automatically. /// Detect automatically.
#[default]
Auto, Auto,
/// Always display colors. /// Always display colors.
@ -102,157 +81,242 @@ enum ColorWhen {
Never, Never,
} }
impl ValueEnum for ColorWhen { impl std::fmt::Display for ColorWhen {
fn value_variants<'a>() -> &'a [Self] { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
&[Self::Auto, Self::Always, Self::Never] f.write_str(match self {
} Self::Auto => "auto",
Self::Always => "always",
fn to_possible_value<'a>(&self) -> Option<PossibleValue> { Self::Never => "never",
match self { })
Self::Auto => Some(PossibleValue::new("auto")),
Self::Always => Some(PossibleValue::new("always")),
Self::Never => Some(PossibleValue::new("never")),
}
} }
} }
pub fn build_cli(include_internal: bool) -> ClapCommand { #[derive(Parser)]
let version = env!("CARGO_PKG_VERSION"); #[command(
let mut app = ClapCommand::new("Colmena") name = "Colmena",
.bin_name("colmena") bin_name = "colmena",
.version(version) author = "Zhaofeng Li <hello@zhaofeng.li>",
.author("Zhaofeng Li <hello@zhaofeng.li>") version = env!("CARGO_PKG_VERSION"),
.about("NixOS deployment tool") about = "NixOS deployment tool",
.long_about(LONG_ABOUT) long_about = LONG_ABOUT,
.arg_required_else_help(true) )]
.arg(Arg::new("config") struct Opts {
.short('f') #[arg(
.long("config") short = 'f',
.value_name("CONFIG") long,
.help("Path to a Hive expression, a flake.nix, or a Nix Flake URI") value_name = "CONFIG",
.long_help(Some(CONFIG_HELP)) help = "Path to a Hive expression, a flake.nix, or a Nix Flake URI",
.display_order(HELP_ORDER_FIRST) long_help = CONFIG_HELP,
display_order = HELP_ORDER_FIRST,
// The default value is a lie (sort of)! global = true,
// )]
// The default behavior is to search upwards from the config: Option<HivePath>,
// current working directory for a file named "flake.nix" #[arg(
// or "hive.nix". This behavior is disabled if --config/-f long,
// is explicitly supplied by the user (occurrences_of > 0). help = "Show debug information for Nix commands",
.default_value("hive.nix") long_help = "Passes --show-trace to Nix commands",
.global(true)) global = true
.arg(Arg::new("show-trace") )]
.long("show-trace") show_trace: bool,
.help("Show debug information for Nix commands") #[arg(
.long_help("Passes --show-trace to Nix commands") long,
.global(true) help = "Allow impure expressions",
.num_args(0)) long_help = "Passes --impure to Nix commands",
.arg(Arg::new("impure") global = true
.long("impure") )]
.help("Allow impure expressions") impure: bool,
.long_help("Passes --impure to Nix commands") #[arg(
.global(true) long,
.num_args(0)) help = "Passes an arbitrary option to Nix commands",
.arg(Arg::new("nix-option") long_help = r#"Passes arbitrary options to Nix commands
.long("nix-option")
.help("Passes an arbitrary option to Nix commands")
.long_help(r#"Passes arbitrary options to Nix commands
This only works when building locally. This only works when building locally.
"#) "#,
.global(true) global = true,
.num_args(2) num_args = 2,
.value_names(["NAME", "VALUE"]) value_names = ["NAME", "VALUE"],
.action(ArgAction::Append)) )]
.arg(Arg::new("color") nix_option: Vec<String>,
.long("color") #[arg(
.help("When to colorize the output") long,
.long_help(r#"When to colorize the output. By default, Colmena enables colorized output when the terminal supports it. 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 <https://bixense.com/clicolors>. It's also possible to specify the preference using environment variables. See <https://bixense.com/clicolors>.
"#) "#,
.display_order(HELP_ORDER_LOW) )]
.value_name("WHEN") color: ColorWhen,
.value_parser(value_parser!(ColorWhen)) #[command(subcommand)]
.default_value("auto") command: Command,
.global(true)); }
if include_internal { #[derive(Subcommand)]
app = app.subcommand( enum Command {
ClapCommand::new("gen-completions") Apply(command::apply::Opts),
.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),
),
);
// deprecated alias
app = app.subcommand(command::eval::deprecated_alias());
#[cfg(debug_assertions)]
register_command!(test_progress, app);
}
register_command!(apply, app);
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
register_command!(apply_local, app); ApplyLocal(command::apply_local::Opts),
register_command!(build, app); #[command(
register_command!(eval, app); about = "Build configurations but not push to remote machines",
register_command!(upload_keys, app); long_about = r#"Build configurations but not push to remote machines
register_command!(exec, app);
register_command!(repl, app);
register_command!(nix_info, app);
// This does _not_ take the --color flag into account (haven't This subcommand behaves as if you invoked `apply` with the `build` goal."#
// parsed yet), only the CLICOLOR environment variable. )]
if clicolors_control::colors_enabled() { Build {
app.color(ColorChoice::Always) #[command(flatten)]
} else { deploy: DeployOpts,
app },
Eval(command::eval::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),
#[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,
#[command(about = "Generate shell auto-completion files (Internal)", hide = true)]
GenCompletions {
shell: Shell,
},
}
async fn get_hive(opts: &Opts) -> ColmenaResult<Hive> {
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 chunks in opts.nix_option.chunks_exact(2) {
let [name, value] = chunks else {
unreachable!()
};
hive.add_nix_option(name.clone(), value.clone());
}
Ok(hive)
} }
pub async fn run() { pub async fn run() {
let mut app = build_cli(true); let opts = Opts::parse();
let matches = app.clone().get_matches();
set_color_pref(matches.get_one("color").unwrap()); set_color_pref(&opts.color);
init_logging(); init_logging();
handle_command!(apply, matches); let hive = get_hive(&opts).await.expect("Failed to get flake or hive");
use crate::troubleshooter::run_wrapped as r;
match opts.command {
Command::Apply(args) => r(command::apply::run(hive, args), opts.config).await,
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
handle_command!("apply-local", apply_local, matches); Command::ApplyLocal(args) => r(command::apply_local::run(hive, args), opts.config).await,
handle_command!(build, matches); Command::Eval(args) => r(command::eval::run(hive, args), opts.config).await,
handle_command!(eval, matches); Command::Exec(args) => r(command::exec::run(hive, args), opts.config).await,
handle_command!("upload-keys", upload_keys, matches); Command::NixInfo => r(command::nix_info::run(), opts.config).await,
handle_command!(exec, matches); Command::Repl => r(command::repl::run(hive), opts.config).await,
handle_command!(repl, matches);
handle_command!("nix-info", nix_info, matches);
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
handle_command!("test-progress", test_progress, matches); Command::TestProgress => r(command::test_progress::run(), opts.config).await,
Command::Build { deploy } => {
if let Some(args) = matches.subcommand_matches("gen-completions") { let args = command::apply::Opts {
return gen_completions(args); deploy,
goal: crate::nix::Goal::Build,
};
r(command::apply::run(hive, args), opts.config).await
}
Command::UploadKeys { deploy } => {
let args = command::apply::Opts {
deploy,
goal: crate::nix::Goal::UploadKeys,
};
r(command::apply::run(hive, args), opts.config).await
}
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) { fn print_completions(shell: Shell, cmd: &mut clap::Command) {
let mut app = build_cli(false); clap_complete::generate(
let shell = args.get_one::<Shell>("shell").unwrap().to_owned(); shell,
cmd,
clap_complete::generate(shell, &mut app, "colmena", &mut std::io::stdout()); cmd.get_name().to_string(),
&mut std::io::stdout(),
);
} }
fn set_color_pref(when: &ColorWhen) { fn set_color_pref(when: &ColorWhen) {
@ -281,13 +345,3 @@ fn init_logging() {
.write_style(style) .write_style(style)
.init(); .init();
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cli_debug_assert() {
build_cli(true).debug_assert()
}
}

View file

@ -1,134 +1,128 @@
use std::env; use std::env;
use std::path::PathBuf; use std::path::PathBuf;
use clap::{ use clap::{builder::ArgPredicate, Args};
builder::{ArgPredicate, PossibleValuesParser, ValueParser},
value_parser, Arg, ArgMatches, Command as ClapCommand,
};
use crate::error::ColmenaError; use crate::error::ColmenaError;
use crate::nix::deployment::{ use crate::nix::{
Deployment, EvaluationNodeLimit, EvaluatorType, Goal, Options, ParallelismLimit, deployment::{Deployment, EvaluationNodeLimit, EvaluatorType, Goal, Options, ParallelismLimit},
node_filter::NodeFilterOpts,
Hive,
}; };
use crate::nix::NodeFilter;
use crate::progress::SimpleProgressOutput; use crate::progress::SimpleProgressOutput;
use crate::util;
pub fn register_deploy_args(command: ClapCommand) -> ClapCommand { #[derive(Debug, Args)]
command pub struct DeployOpts {
.arg(Arg::new("eval-node-limit") #[arg(
.long("eval-node-limit") value_name = "LIMIT",
.value_name("LIMIT") default_value_t,
.help("Evaluation node limit") long,
.long_help(r#"Limits the maximum number of hosts to be evaluated at once. 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. 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. Set to 0 to disable the limit.
"#) "#
.default_value("auto") )]
.num_args(1) eval_node_limit: EvaluationNodeLimit,
.value_parser(ValueParser::new(|s: &str| -> Result<EvaluationNodeLimit, String> { #[arg(
if s == "auto" { value_name = "LIMIT",
return Ok(EvaluationNodeLimit::Heuristic); 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::<usize>() { Set to 0 to disable parallelism limit.
Ok(0) => Ok(EvaluationNodeLimit::None), "#
Ok(n) => Ok(EvaluationNodeLimit::Manual(n)), )]
Err(_) => Err(String::from("The value must be a valid number")), parallel: usize,
} #[arg(
}))) long,
.arg(Arg::new("parallel") help = "Create GC roots for built profiles",
.short('p') long_help = r#"Create GC roots for built profiles.
.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.
The built system profiles will be added as GC roots so that they will not be removed by the garbage collector. 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. The links will be created under .gcroots in the directory the Hive configuration is located.
"#) "#
.num_args(0)) )]
.arg(Arg::new("verbose") keep_result: bool,
.short('v') #[arg(
.long("verbose") short,
.help("Be verbose") long,
.long_help("Deactivates the progress spinner and prints every line of output.") help = "Be verbose",
.num_args(0)) long_help = "Deactivates the progress spinner and prints every line of output."
.arg(Arg::new("no-keys") )]
.long("no-keys") verbose: bool,
.help("Do not upload keys") #[arg(
.long_help(r#"Do not upload secret keys set in `deployment.keys`. 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. 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`. To upload keys without building or deploying the rest of the configuration, use `colmena upload-keys`.
"#) "#
.num_args(0)) )]
.arg(Arg::new("reboot") no_keys: bool,
.long("reboot") #[arg(
.help("Reboot nodes after activation") long,
.long_help("Reboots nodes after activation and waits for them to come back up.") help = "Reboot nodes after activation",
.num_args(0)) long_help = "Reboots nodes after activation and waits for them to come back up."
.arg(Arg::new("no-substitute") )]
.long("no-substitute") reboot: bool,
.alias("no-substitutes") #[arg(
.help("Do not use substitutes") long,
.long_help("Disables the use of substituters when copying closures to the remote host.") alias = "no-substitutes",
.num_args(0)) help = "Do not use substitutes",
.arg(Arg::new("no-gzip") long_help = "Disables the use of substituters when copying closures to the remote host."
.long("no-gzip") )]
.help("Do not use gzip") no_substitute: bool,
.long_help("Disables the use of gzip when copying closures to the remote host.") #[arg(
.num_args(0)) long,
.arg(Arg::new("build-on-target") help = "Do not use gzip",
.long("build-on-target") long_help = "Disables the use of gzip when copying closures to the remote host."
.help("Build the system profiles on the target nodes") )]
.long_help(r#"Build the system profiles on the target nodes themselves. 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. 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`. This overrides per-node perferences set in `deployment.buildOnTarget`.
To temporarily disable remote build on all nodes, use `--no-build-on-target`. To temporarily disable remote build on all nodes, use `--no-build-on-target`.
"#) "#
.num_args(0)) )]
.arg(Arg::new("no-build-on-target") build_on_target: bool,
.long("no-build-on-target") #[arg(long, hide = true)]
.hide(true) no_build_on_target: bool,
.num_args(0)) #[arg(
.arg(Arg::new("force-replace-unknown-profiles") long,
.long("force-replace-unknown-profiles") help = "Ignore all targeted nodes deployment.replaceUnknownProfiles setting",
.help("Ignore all targeted nodes deployment.replaceUnknownProfiles setting") long_help = r#"If `deployment.replaceUnknownProfiles` is set for a target, using this switch
.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."#
will treat deployment.replaceUnknownProfiles as though it was set true and perform unknown profile replacement."#) )]
.num_args(0)) force_replace_unknown_profiles: bool,
.arg(Arg::new("evaluator") #[arg(
.long("evaluator") long,
.help("The evaluator to use (experimental)") default_value_t,
.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. 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."#) This is an experimental feature."#
.default_value("chunked") )]
.value_parser(value_parser!(EvaluatorType))) evaluator: EvaluatorType,
#[command(flatten)]
node_filter: NodeFilterOpts,
} }
pub fn subcommand() -> ClapCommand { #[derive(Debug, Args)]
let command = ClapCommand::new("apply") #[command(name = "apply", about = "Apply configurations on remote machines")]
.about("Apply configurations on remote machines") pub struct Opts {
.arg( #[arg(
Arg::new("goal") help = "Deployment goal",
.help("Deployment goal") long_help = r#"The goal of the deployment.
.long_help(
r#"The goal of the deployment.
Same as the targets for switch-to-configuration, with the following extra pseudo-goals: Same as the targets for switch-to-configuration, with the following extra pseudo-goals:
@ -138,42 +132,38 @@ 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. `switch` is the default goal unless `--reboot` is passed, in which case `boot` is the default.
"#, "#,
) default_value_t,
.default_value("switch") default_value_if("reboot", ArgPredicate::IsPresent, Some("boot"))
.default_value_if("reboot", ArgPredicate::IsPresent, Some("boot")) )]
.default_value("switch") pub goal: Goal,
.index(1) #[command(flatten)]
.value_parser(PossibleValuesParser::new([ pub deploy: DeployOpts,
"build",
"push",
"switch",
"boot",
"test",
"dry-activate",
"keys",
])),
);
let command = register_deploy_args(command);
util::register_selector_args(command)
} }
pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(), ColmenaError> { pub async fn run(hive: Hive, opts: Opts) -> Result<(), ColmenaError> {
let hive = util::hive_from_args(local_args).await?;
let ssh_config = env::var("SSH_CONFIG_FILE").ok().map(PathBuf::from); let ssh_config = env::var("SSH_CONFIG_FILE").ok().map(PathBuf::from);
// FIXME: Just get_one::<Goal> let Opts {
let goal_arg = local_args.get_one::<String>("goal").unwrap(); goal,
let goal = Goal::from_str(goal_arg).unwrap(); 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;
// FIXME: Just get_one::<NodeFilter> if node_filter.on.is_none() && goal != Goal::Build {
let filter = local_args
.get_one::<String>("on")
.map(NodeFilter::new)
.transpose()?;
if filter.is_none() && goal != Goal::Build {
// User did not specify node, we should check meta and see rules // User did not specify node, we should check meta and see rules
let meta = hive.get_meta_config().await?; let meta = hive.get_meta_config().await?;
if !meta.allow_apply_all { if !meta.allow_apply_all {
@ -184,11 +174,15 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(
} }
let targets = hive let targets = hive
.select_nodes(filter, ssh_config, goal.requires_target_host()) .select_nodes(
node_filter.on.clone(),
ssh_config,
goal.requires_target_host(),
)
.await?; .await?;
let n_targets = targets.len(); 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 mut output = SimpleProgressOutput::new(verbose);
let progress = output.get_sender(); let progress = output.get_sender();
@ -197,27 +191,20 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(
// FIXME: Configure limits // FIXME: Configure limits
let options = { let options = {
let mut options = Options::default(); let mut options = Options::default();
options.set_substituters_push(!local_args.get_flag("no-substitute")); options.set_substituters_push(!no_substitute);
options.set_gzip(!local_args.get_flag("no-gzip")); options.set_gzip(!no_gzip);
options.set_upload_keys(!local_args.get_flag("no-keys")); options.set_upload_keys(!no_keys);
options.set_reboot(local_args.get_flag("reboot")); options.set_reboot(reboot);
options.set_force_replace_unknown_profiles( options.set_force_replace_unknown_profiles(force_replace_unknown_profiles);
local_args.get_flag("force-replace-unknown-profiles"), options.set_evaluator(evaluator);
);
options.set_evaluator(
local_args
.get_one::<EvaluatorType>("evaluator")
.unwrap()
.to_owned(),
);
if local_args.get_flag("keep-result") { if keep_result {
options.set_create_gc_roots(true); 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); 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); options.set_force_build_on_target(true);
} }
@ -226,7 +213,7 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(
deployment.set_options(options); 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"); log::error!("--no-keys cannot be used when the goal is to upload keys");
quit::with_code(1); quit::with_code(1);
} }
@ -234,23 +221,17 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(
let parallelism_limit = { let parallelism_limit = {
let mut limit = ParallelismLimit::default(); let mut limit = ParallelismLimit::default();
limit.set_apply_limit({ limit.set_apply_limit({
let limit = local_args.get_one::<usize>("parallel").unwrap().to_owned(); if parallel == 0 {
if limit == 0 {
n_targets n_targets
} else { } else {
limit parallel
} }
}); });
limit limit
}; };
let evaluation_node_limit = local_args
.get_one::<EvaluationNodeLimit>("eval-node-limit")
.unwrap()
.to_owned();
deployment.set_parallelism_limit(parallelism_limit); 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(),); let (deployment, output) = tokio::join!(deployment.execute(), output.run_until_completion(),);

View file

@ -1,66 +1,70 @@
use regex::Regex; use regex::Regex;
use std::collections::HashMap; use std::collections::HashMap;
use clap::{builder::PossibleValuesParser, Arg, ArgMatches, Command as ClapCommand}; use clap::Args;
use tokio::fs; use tokio::fs;
use crate::error::ColmenaError; use crate::error::ColmenaError;
use crate::nix::deployment::{Deployment, Goal, Options, TargetNode}; use crate::nix::deployment::{Deployment, Goal, Options, TargetNode};
use crate::nix::Hive;
use crate::nix::{host::Local as LocalHost, NodeName}; use crate::nix::{host::Local as LocalHost, NodeName};
use crate::progress::SimpleProgressOutput; use crate::progress::SimpleProgressOutput;
use crate::util;
pub fn subcommand() -> ClapCommand { #[derive(Debug, Args)]
ClapCommand::new("apply-local") #[command(
.about("Apply configurations on the local machine") name = "apply-local",
.arg(Arg::new("goal") about = "Apply configurations on the local machine"
.help("Deployment goal") )]
.long_help("Same as the targets for switch-to-configuration.\n\"push\" is noop in apply-local.") pub struct Opts {
.default_value("switch") #[arg(
.index(1) help = "Deployment goal",
.value_parser(PossibleValuesParser::new([ value_name = "GOAL",
"push", default_value_t,
"switch", long_help = "Same as the targets for switch-to-configuration.\n\"push\" is noop in apply-local."
"boot", )]
"test", goal: Goal,
"dry-activate", #[arg(long, help = "Attempt to escalate privileges if not run as root")]
"keys", sudo: bool,
]))) #[arg(
.arg(Arg::new("sudo") short,
.long("sudo") long,
.help("Attempt to escalate privileges if not run as root") help = "Be verbose",
.num_args(0)) long_help = "Deactivates the progress spinner and prints every line of output."
.arg(Arg::new("verbose") )]
.short('v') verbose: bool,
.long("verbose") #[arg(
.help("Be verbose") long,
.long_help("Deactivates the progress spinner and prints every line of output.") help = "Do not deploy keys",
.num_args(0)) long_help = r#"Do not deploy secret keys set in `deployment.keys`.
.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`.
By default, Colmena will deploy keys set in `deployment.keys` before activating the profile on this host. By default, Colmena will deploy keys set in `deployment.keys` before activating the profile on this host.
"#) "#
.num_args(0)) )]
.arg(Arg::new("node") no_keys: bool,
.long("node") #[arg(long, help = "Override the node name to use")]
.value_name("NODE") node: Option<String>,
.help("Override the node name to use") #[arg(
.num_args(1)) long,
value_name = "COMMAND",
// Removed hide = true,
.arg(Arg::new("sudo-command") help = "Removed: Configure deployment.privilegeEscalationCommand in node configuration"
.long("sudo-command") )]
.value_name("COMMAND") sudo_command: Option<String>,
.help("Removed: Configure deployment.privilegeEscalationCommand in node configuration")
.hide(true)
.num_args(1))
} }
pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(), ColmenaError> { pub async fn run(
if local_args.contains_id("sudo-command") { hive: Hive,
Opts {
goal,
sudo,
verbose,
no_keys,
node,
sudo_command,
}: 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."); log::error!("--sudo-command has been removed. Please configure it in deployment.privilegeEscalationCommand in the node configuration.");
quit::with_code(1); quit::with_code(1);
} }
@ -77,31 +81,22 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(
quit::with_code(5); quit::with_code(5);
} }
let escalate_privileges = local_args.get_flag("sudo"); let verbose = verbose || sudo; // cannot use spinners with interactive sudo
let verbose = local_args.get_flag("verbose") || escalate_privileges; // cannot use spinners with interactive sudo
{ {
let euid: u32 = unsafe { libc::geteuid() }; 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!("Colmena was not started by root. This is probably not going to work.");
log::warn!("Hint: Add the --sudo flag."); log::warn!("Hint: Add the --sudo flag.");
} }
} }
let hive = util::hive_from_args(local_args).await.unwrap(); let hostname = NodeName::new(node.unwrap_or_else(|| {
let hostname = {
let s = if local_args.contains_id("node") {
local_args.get_one::<String>("node").unwrap().to_owned()
} else {
hostname::get() hostname::get()
.expect("Could not get hostname") .expect("Could not get hostname")
.to_string_lossy() .to_string_lossy()
.into_owned() .into_owned()
}; }))?;
NodeName::new(s)?
};
let goal = Goal::from_str(local_args.get_one::<String>("goal").unwrap()).unwrap();
let target = { let target = {
if let Some(info) = hive.deployment_info_single(&hostname).await.unwrap() { if let Some(info) = hive.deployment_info_single(&hostname).await.unwrap() {
@ -115,7 +110,7 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(
quit::with_code(2); quit::with_code(2);
} }
let mut host = LocalHost::new(nix_options); let mut host = LocalHost::new(nix_options);
if escalate_privileges { if sudo {
let command = info.privilege_escalation_command().to_owned(); let command = info.privilege_escalation_command().to_owned();
host.set_privilege_escalation_command(Some(command)); host.set_privilege_escalation_command(Some(command));
} }
@ -140,13 +135,13 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(
let options = { let options = {
let mut options = Options::default(); let mut options = Options::default();
options.set_upload_keys(!local_args.get_flag("no-keys")); options.set_upload_keys(!no_keys);
options options
}; };
deployment.set_options(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?; deployment?;
output?; output?;

View file

@ -1,27 +0,0 @@
use clap::{builder::PossibleValuesParser, Arg, Command as ClapCommand};
use crate::util;
use super::apply;
pub use super::apply::run;
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
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),
);
let command = apply::register_deploy_args(command);
util::register_selector_args(command)
}

View file

@ -1,70 +1,47 @@
use std::path::PathBuf; use std::path::PathBuf;
use clap::{value_parser, Arg, ArgMatches, Command as ClapCommand}; use clap::Args;
use crate::error::ColmenaError; use crate::error::ColmenaError;
use crate::util; use crate::nix::Hive;
pub fn subcommand() -> ClapCommand { #[derive(Debug, Args)]
subcommand_gen("eval") #[command(
} name = "eval",
alias = "introspect",
pub fn deprecated_alias() -> ClapCommand { about = "Evaluate an expression using the complete configuration",
subcommand_gen("introspect").hide(true) long_about = r#"Evaluate an expression using the complete configuration
}
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
Your expression should take an attribute set with keys `pkgs`, `lib` and `nodes` (like a NixOS module) and return a JSON-serializable value. 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: For example, to retrieve the configuration of one node, you may write something like:
{ nodes, ... }: nodes.node-a.config.networking.hostName { nodes, ... }: nodes.node-a.config.networking.hostName
"#) "#
.arg(Arg::new("expression_file") )]
.index(1) pub struct Opts {
.value_name("FILE") #[arg(short = 'E', value_name = "EXPRESSION", help = "The Nix expression")]
.help("The .nix file containing the expression") expression: Option<String>,
.num_args(1) #[arg(long, help = "Actually instantiate the expression")]
.value_parser(value_parser!(PathBuf))) instantiate: bool,
.arg(Arg::new("expression") #[arg(
.short('E') value_name = "FILE",
.value_name("EXPRESSION") help = "The .nix file containing the expression",
.help("The Nix expression") conflicts_with("expression")
.num_args(1)) )]
.arg(Arg::new("instantiate") expression_file: Option<PathBuf>,
.long("instantiate")
.help("Actually instantiate the expression")
.num_args(0))
} }
pub async fn run(global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(), ColmenaError> { pub async fn run(
if let Some("introspect") = global_args.subcommand_name() { hive: Hive,
log::warn!( Opts {
"`colmena introspect` has been renamed to `colmena eval`. Please update your scripts." expression,
); instantiate,
} expression_file,
}: Opts,
let hive = util::hive_from_args(local_args).await?; ) -> Result<(), ColmenaError> {
let expression = expression_file
if !(local_args.contains_id("expression") ^ local_args.contains_id("expression_file")) { .map(|path| {
log::error!("Either an expression (-E) or a .nix file containing an expression should be specified, not both.");
quit::with_code(1);
}
let expression = if local_args.contains_id("expression") {
local_args
.get_one::<String>("expression")
.unwrap()
.to_owned()
} else {
let path = local_args
.get_one::<PathBuf>("expression_file")
.unwrap()
.to_owned();
format!( format!(
"import {}", "import {}",
path.canonicalize() path.canonicalize()
@ -72,9 +49,14 @@ pub async fn run(global_args: &ArgMatches, local_args: &ArgMatches) -> Result<()
.to_str() .to_str()
.unwrap() .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?; let result = hive.introspect(expression, instantiate).await?;
if instantiate { if instantiate {

View file

@ -2,94 +2,78 @@ use std::env;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use clap::{value_parser, Arg, ArgMatches, Command as ClapCommand}; use clap::Args;
use futures::future::join_all; use futures::future::join_all;
use tokio::sync::Semaphore; use tokio::sync::Semaphore;
use crate::error::ColmenaError; use crate::error::ColmenaError;
use crate::job::{JobMonitor, JobState, JobType}; use crate::job::{JobMonitor, JobState, JobType};
use crate::nix::NodeFilter; use crate::nix::node_filter::NodeFilterOpts;
use crate::nix::Hive;
use crate::progress::SimpleProgressOutput; use crate::progress::SimpleProgressOutput;
use crate::util; use crate::util;
pub fn subcommand() -> ClapCommand { #[derive(Debug, Args)]
let command = ClapCommand::new("exec") #[command(name = "exec", about = "Run a command on remote machines")]
.about("Run a command on remote machines") pub struct Opts {
.arg( #[arg(
Arg::new("parallel") short,
.short('p') long,
.long("parallel") default_value_t = 0,
.value_name("LIMIT") value_name = "LIMIT",
.help("Deploy parallelism limit") help = "Deploy parallelism limit",
.long_help( long_help = r#"Limits the maximum number of hosts to run the command in parallel.
r#"Limits the maximum number of hosts to run the command in parallel.
In `colmena exec`, the parallelism limit is disabled (0) by default. In `colmena exec`, the parallelism limit is disabled (0) by default.
"#, "#
) )]
.default_value("0") parallel: usize,
.num_args(1) #[arg(
.value_parser(value_parser!(usize)), short,
) long,
.arg( help = "Be verbose",
Arg::new("verbose") long_help = "Deactivates the progress spinner and prints every line of output."
.short('v') )]
.long("verbose") verbose: bool,
.help("Be verbose") #[command(flatten)]
.long_help("Deactivates the progress spinner and prints every line of output.") nodes: NodeFilterOpts,
.num_args(0), #[arg(
) trailing_var_arg = true,
.arg( required = true,
Arg::new("command") value_name = "COMMAND",
.value_name("COMMAND") help = "Command",
.trailing_var_arg(true) long_help = r#"Command to run
.help("Command")
.required(true)
.num_args(1..)
.long_help(
r#"Command to run
It's recommended to use -- to separate Colmena options from the command to run. For example: 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 colmena exec --on @routers -- tcpdump -vni any ip[9] == 89
"#, "#
), )]
); command: Vec<String>,
util::register_selector_args(command)
} }
pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(), ColmenaError> { pub async fn run(
let hive = util::hive_from_args(local_args).await?; hive: Hive,
Opts {
parallel,
verbose,
nodes,
command,
}: Opts,
) -> Result<(), ColmenaError> {
let ssh_config = env::var("SSH_CONFIG_FILE").ok().map(PathBuf::from); let ssh_config = env::var("SSH_CONFIG_FILE").ok().map(PathBuf::from);
// FIXME: Just get_one::<NodeFilter> let mut targets = hive.select_nodes(nodes.on, ssh_config, true).await?;
let filter = local_args
.get_one::<String>("on")
.map(NodeFilter::new)
.transpose()?;
let mut targets = hive.select_nodes(filter, ssh_config, true).await?; let parallel_sp = Arc::new(if parallel > 0 {
Some(Semaphore::new(parallel))
let parallel_sp = Arc::new({
let limit = local_args.get_one::<usize>("parallel").unwrap().to_owned();
if limit > 0 {
Some(Semaphore::new(limit))
} else { } else {
None None
}
}); });
let command: Arc<Vec<String>> = Arc::new( let command = Arc::new(command);
local_args
.get_many::<String>("command")
.unwrap()
.cloned()
.collect(),
);
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()); let (mut monitor, meta) = JobMonitor::new(output.get_sender());
@ -102,7 +86,7 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(
for (name, target) in targets.drain() { for (name, target) in targets.drain() {
let parallel_sp = parallel_sp.clone(); let parallel_sp = parallel_sp.clone();
let command = command.clone(); let command = Arc::clone(&command);
let mut host = target.into_host().unwrap(); let mut host = target.into_host().unwrap();

View file

@ -1,10 +1,8 @@
pub mod apply; pub mod apply;
pub mod build;
pub mod eval; pub mod eval;
pub mod exec; pub mod exec;
pub mod nix_info; pub mod nix_info;
pub mod repl; pub mod repl;
pub mod upload_keys;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub mod apply_local; pub mod apply_local;

View file

@ -1,14 +1,8 @@
use clap::{ArgMatches, Command as ClapCommand}; use crate::error::ColmenaResult;
use crate::error::ColmenaError;
use crate::nix::evaluator::nix_eval_jobs::get_pinned_nix_eval_jobs; use crate::nix::evaluator::nix_eval_jobs::get_pinned_nix_eval_jobs;
use crate::nix::NixCheck; use crate::nix::NixCheck;
pub fn subcommand() -> ClapCommand { pub async fn run() -> ColmenaResult<()> {
ClapCommand::new("nix-info").about("Show information about the current Nix installation")
}
pub async fn run(_global_args: &ArgMatches, _local_args: &ArgMatches) -> Result<(), ColmenaError> {
let check = NixCheck::detect().await; let check = NixCheck::detect().await;
check.print_version_info(); check.print_version_info();
check.print_flakes_info(false); check.print_flakes_info(false);

View file

@ -1,31 +1,16 @@
use std::io::Write; use std::io::Write;
use clap::{ArgMatches, Command as ClapCommand};
use tempfile::Builder as TempFileBuilder; use tempfile::Builder as TempFileBuilder;
use tokio::process::Command; use tokio::process::Command;
use crate::error::ColmenaError; use crate::error::ColmenaResult;
use crate::nix::info::NixCheck; use crate::nix::info::NixCheck;
use crate::util; use crate::nix::Hive;
pub fn subcommand() -> ClapCommand { pub async fn run(hive: Hive) -> ColmenaResult<()> {
ClapCommand::new("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 async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(), ColmenaError> {
let nix_check = NixCheck::detect().await; let nix_check = NixCheck::detect().await;
let nix_version = nix_check.version().expect("Could not detect Nix version"); let nix_version = nix_check.version().expect("Could not detect Nix version");
let hive = util::hive_from_args(local_args).await?;
let expr = hive.get_repl_expression(); let expr = hive.get_repl_expression();
let mut expr_file = TempFileBuilder::new() let mut expr_file = TempFileBuilder::new()

View file

@ -1,6 +1,5 @@
use std::time::Duration; use std::time::Duration;
use clap::{ArgMatches, Command as ClapCommand};
use tokio::time; use tokio::time;
use crate::error::{ColmenaError, ColmenaResult}; use crate::error::{ColmenaError, ColmenaResult};
@ -14,13 +13,7 @@ macro_rules! node {
}; };
} }
pub fn subcommand() -> ClapCommand { pub async fn run() -> Result<(), ColmenaError> {
ClapCommand::new("test-progress")
.about("Run progress spinner tests")
.hide(true)
}
pub async fn run(_global_args: &ArgMatches, _local_args: &ArgMatches) -> Result<(), ColmenaError> {
let mut output = SpinnerOutput::new(); let mut output = SpinnerOutput::new();
let (monitor, meta) = JobMonitor::new(output.get_sender()); let (monitor, meta) = JobMonitor::new(output.get_sender());

View file

@ -1,27 +0,0 @@
use clap::{builder::PossibleValuesParser, Arg, Command as ClapCommand};
use crate::util;
use super::apply;
pub use super::apply::run;
pub fn subcommand() -> ClapCommand {
let command = ClapCommand::new("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),
);
let command = apply::register_deploy_args(command);
util::register_selector_args(command)
}

View file

@ -1,7 +1,9 @@
//! Deployment goals. //! Deployment goals.
use std::str::FromStr;
/// The goal of a deployment. /// The goal of a deployment.
#[derive(Debug, Copy, Clone, PartialEq, Eq)] #[derive(Debug, Copy, Clone, Default, PartialEq, Eq, clap::ValueEnum)]
pub enum Goal { pub enum Goal {
/// Build the configurations only. /// Build the configurations only.
Build, Build,
@ -10,6 +12,7 @@ pub enum Goal {
Push, Push,
/// Make the configuration the boot default and activate now. /// Make the configuration the boot default and activate now.
#[default]
Switch, Switch,
/// Make the configuration the boot default. /// Make the configuration the boot default.
@ -25,20 +28,37 @@ pub enum Goal {
UploadKeys, UploadKeys,
} }
impl Goal { impl FromStr for Goal {
pub fn from_str(s: &str) -> Option<Self> { type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s { match s {
"build" => Some(Self::Build), "build" => Ok(Self::Build),
"push" => Some(Self::Push), "push" => Ok(Self::Push),
"switch" => Some(Self::Switch), "switch" => Ok(Self::Switch),
"boot" => Some(Self::Boot), "boot" => Ok(Self::Boot),
"test" => Some(Self::Test), "test" => Ok(Self::Test),
"dry-activate" => Some(Self::DryActivate), "dry-activate" => Ok(Self::DryActivate),
"keys" => Some(Self::UploadKeys), "keys" => Ok(Self::UploadKeys),
_ => None, _ => 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> { pub fn as_str(&self) -> Option<&'static str> {
use Goal::*; use Goal::*;
match self { match self {

View file

@ -1,5 +1,7 @@
//! Parallelism limits. //! Parallelism limits.
use std::str::FromStr;
use tokio::sync::Semaphore; use tokio::sync::Semaphore;
/// Amount of RAM reserved for the system, in MB. /// 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 simple heuristic based on remaining memory in the system
/// - A supplied number /// - A supplied number
/// - No limit at all /// - No limit at all
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug, Default)]
pub enum EvaluationNodeLimit { pub enum EvaluationNodeLimit {
/// Use a naive heuristic based on available memory. /// Use a naive heuristic based on available memory.
#[default]
Heuristic, Heuristic,
/// Supply the maximum number of nodes. /// Supply the maximum number of nodes.
@ -67,9 +70,28 @@ pub enum EvaluationNodeLimit {
None, None,
} }
impl Default for EvaluationNodeLimit { impl FromStr for EvaluationNodeLimit {
fn default() -> Self { type Err = &'static str;
Self::Heuristic fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "auto" {
return Ok(EvaluationNodeLimit::Heuristic);
}
match s.parse::<usize>() {
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}"),
}
} }
} }

View file

@ -1,6 +1,6 @@
//! Deployment options. //! Deployment options.
use clap::{builder::PossibleValue, ValueEnum}; use clap::ValueEnum;
use crate::nix::CopyOptions; use crate::nix::CopyOptions;
@ -36,12 +36,22 @@ pub struct Options {
} }
/// Which evaluator to use. /// Which evaluator to use.
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq, Default, ValueEnum)]
pub enum EvaluatorType { pub enum EvaluatorType {
#[default]
Chunked, Chunked,
Streaming, 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 { impl Options {
pub fn set_substituters_push(&mut self, value: bool) { pub fn set_substituters_push(&mut self, value: bool) {
self.substituters_push = value; 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<PossibleValue> {
match self {
Self::Chunked => Some(PossibleValue::new("chunked")),
Self::Streaming => Some(PossibleValue::new("streaming")),
}
}
}

View file

@ -53,10 +53,10 @@ impl Flake {
} }
/// Creates a flake from a Flake URI. /// Creates a flake from a Flake URI.
pub async fn from_uri(uri: String) -> ColmenaResult<Self> { pub async fn from_uri(uri: impl AsRef<str>) -> ColmenaResult<Self> {
NixCheck::require_flake_support().await?; NixCheck::require_flake_support().await?;
let metadata = FlakeMetadata::resolve(&uri).await?; let metadata = FlakeMetadata::resolve(uri.as_ref()).await?;
Ok(Self { Ok(Self {
metadata, metadata,

View file

@ -6,6 +6,7 @@ mod tests;
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::AsRef; use std::convert::AsRef;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str::FromStr;
use tokio::process::Command; use tokio::process::Command;
use tokio::sync::OnceCell; use tokio::sync::OnceCell;
@ -16,7 +17,7 @@ use super::{
Flake, MetaConfig, NixExpression, NixFlags, NodeConfig, NodeFilter, NodeName, Flake, MetaConfig, NixExpression, NixFlags, NodeConfig, NodeFilter, NodeName,
ProfileDerivation, SerializedNixExpression, StorePath, ProfileDerivation, SerializedNixExpression, StorePath,
}; };
use crate::error::ColmenaResult; use crate::error::{ColmenaError, ColmenaResult};
use crate::job::JobHandle; use crate::job::JobHandle;
use crate::util::{CommandExecution, CommandExt}; use crate::util::{CommandExecution, CommandExt};
use assets::Assets; use assets::Assets;
@ -32,6 +33,36 @@ pub enum HivePath {
Legacy(PathBuf), Legacy(PathBuf),
} }
impl FromStr for HivePath {
type Err = ColmenaError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
// TODO: check for escaped colon maybe?
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");
std::thread::spawn(move || handle.block_on(fut))
.join()
.expect("Failed to join future")
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct Hive { pub struct Hive {
/// Path to the hive. /// Path to the hive.

View file

@ -3,12 +3,33 @@
use std::collections::HashSet; use std::collections::HashSet;
use std::convert::AsRef; use std::convert::AsRef;
use std::iter::{FromIterator, Iterator}; use std::iter::{FromIterator, Iterator};
use std::str::FromStr;
use clap::Args;
use glob::Pattern as GlobPattern; use glob::Pattern as GlobPattern;
use super::{ColmenaError, ColmenaResult, NodeConfig, NodeName}; use super::{ColmenaError, ColmenaResult, NodeConfig, NodeName};
#[derive(Debug, Default, 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<NodeFilter>,
}
/// A node filter containing a list of rules. /// A node filter containing a list of rules.
#[derive(Clone, Debug)]
pub struct NodeFilter { pub struct NodeFilter {
rules: Vec<Rule>, rules: Vec<Rule>,
} }
@ -16,7 +37,7 @@ pub struct NodeFilter {
/// A filter rule. /// A filter rule.
/// ///
/// The filter rules are OR'd together. /// The filter rules are OR'd together.
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Clone, Eq, PartialEq)]
enum Rule { enum Rule {
/// Matches a node's attribute name. /// Matches a node's attribute name.
MatchName(GlobPattern), MatchName(GlobPattern),
@ -25,6 +46,13 @@ enum Rule {
MatchTag(GlobPattern), MatchTag(GlobPattern),
} }
impl FromStr for NodeFilter {
type Err = ColmenaError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
impl NodeFilter { impl NodeFilter {
/// Creates a new filter using an expression passed using `--on`. /// Creates a new filter using an expression passed using `--on`.
pub fn new<S: AsRef<str>>(filter: S) -> ColmenaResult<Self> { pub fn new<S: AsRef<str>>(filter: S) -> ColmenaResult<Self> {

View file

@ -5,28 +5,22 @@
use std::env; use std::env;
use std::future::Future; use std::future::Future;
use clap::{parser::ValueSource as ClapValueSource, ArgMatches};
use snafu::ErrorCompat; 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. /// Runs a closure and tries to troubleshoot if it returns an error.
pub async fn run_wrapped<'a, F, U, T>( pub async fn run_wrapped<'a, F, T>(f: F, hive_config: Option<HivePath>) -> T
global_args: &'a ArgMatches,
local_args: &'a ArgMatches,
f: U,
) -> T
where where
U: FnOnce(&'a ArgMatches, &'a ArgMatches) -> F,
F: Future<Output = Result<T, ColmenaError>>, F: Future<Output = Result<T, ColmenaError>>,
{ {
match f(global_args, local_args).await { match f.await {
Ok(r) => r, Ok(r) => r,
Err(error) => { Err(error) => {
log::error!("-----"); log::error!("-----");
log::error!("Operation failed with error: {}", error); log::error!("Operation failed with error: {}", error);
if let Err(own_error) = troubleshoot(global_args, local_args, &error) { if let Err(own_error) = troubleshoot(hive_config, &error) {
log::error!( log::error!(
"Error occurred while trying to troubleshoot another error: {}", "Error occurred while trying to troubleshoot another error: {}",
own_error own_error
@ -39,17 +33,13 @@ where
} }
} }
fn troubleshoot( fn troubleshoot(hive_config: Option<HivePath>, error: &ColmenaError) -> Result<(), ColmenaError> {
global_args: &ArgMatches,
_local_args: &ArgMatches,
error: &ColmenaError,
) -> Result<(), ColmenaError> {
if let ColmenaError::NoFlakesSupport = error { if let ColmenaError::NoFlakesSupport = error {
// People following the tutorial might put hive.nix directly // People following the tutorial might put hive.nix directly
// in their Colmena checkout, and encounter NoFlakesSupport // in their Colmena checkout, and encounter NoFlakesSupport
// because Colmena always prefers flake.nix when it exists. // 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()?; let cwd = env::current_dir()?;
if cwd.join("flake.nix").is_file() && cwd.join("hive.nix").is_file() { if cwd.join("flake.nix").is_file() && cwd.join("hive.nix").is_file() {
eprintln!( eprintln!(
@ -75,8 +65,5 @@ fn troubleshoot(
} }
fn backtrace_enabled() -> bool { fn backtrace_enabled() -> bool {
match env::var("RUST_BACKTRACE") { matches!(env::var("RUST_BACKTRACE"), Ok(backtrace_conf) if backtrace_conf != "0")
Ok(backtrace_conf) => backtrace_conf != "0",
_ => false,
}
} }

View file

@ -1,9 +1,8 @@
use std::convert::TryFrom; use std::convert::TryFrom;
use std::path::PathBuf;
use std::process::Stdio; use std::process::Stdio;
use async_trait::async_trait; use async_trait::async_trait;
use clap::{parser::ValueSource as ClapValueSource, Arg, ArgMatches, Command as ClapCommand};
use futures::future::join3; use futures::future::join3;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use tokio::io::{AsyncBufReadExt, AsyncRead, BufReader}; use tokio::io::{AsyncBufReadExt, AsyncRead, BufReader};
@ -12,7 +11,7 @@ use tokio::process::Command;
use super::error::{ColmenaError, ColmenaResult}; use super::error::{ColmenaError, ColmenaResult};
use super::job::JobHandle; use super::job::JobHandle;
use super::nix::deployment::TargetNodeMap; use super::nix::deployment::TargetNodeMap;
use super::nix::{Flake, Hive, HivePath, StorePath}; use super::nix::StorePath;
const NEWLINE: u8 = 0xa; const NEWLINE: u8 = 0xa;
@ -192,123 +191,6 @@ impl CommandExt for CommandExecution {
} }
} }
pub async fn hive_from_args(args: &ArgMatches) -> ColmenaResult<Hive> {
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::<String>("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<Hive> {
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::<String>("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)
}
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<R>( pub async fn capture_stream<R>(
mut stream: BufReader<R>, mut stream: BufReader<R>,
job: Option<JobHandle>, job: Option<JobHandle>,