From 60d64758976f66b3d87b9a87c899fff84331de1c Mon Sep 17 00:00:00 2001 From: Zhaofeng Li Date: Tue, 29 Dec 2020 11:31:19 -0800 Subject: [PATCH] Traverse up to find hive.nix by default, and other CLI ergonomics fixes --- Cargo.lock | 50 ++++++++++++++++++ Cargo.toml | 1 + default.nix | 2 +- src/command/apply.rs | 16 +++--- src/command/apply_local.rs | 36 +++++-------- src/command/build.rs | 13 +++-- src/command/introspect.rs | 16 ++---- src/main.rs | 45 ++++++++++++++-- src/nix/host.rs | 2 +- src/nix/mod.rs | 23 +-------- src/util.rs | 103 ++++++++++++++++++++++++++++++------- 11 files changed, 212 insertions(+), 95 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 795b67d..34ca21e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,14 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "aho-corasick" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +dependencies = [ + "memchr", +] + [[package]] name = "ansi_term" version = "0.11.0" @@ -83,6 +92,7 @@ dependencies = [ "async-trait", "clap", "console", + "env_logger", "futures", "glob", "hostname", @@ -125,6 +135,19 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "env_logger" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26ecb66b4bdca6c1409b40fb255eefc2bd4f6d135dab3c3124f80ffa2a9661e" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + [[package]] name = "futures" version = "0.3.8" @@ -257,6 +280,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "humantime" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c1ad908cc71012b7bea4d0c53ba96a8cba9962f048fa68d143376143d863b7a" + [[package]] name = "indicatif" version = "0.15.0" @@ -541,7 +570,10 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c" dependencies = [ + "aho-corasick", + "memchr", "regex-syntax", + "thread_local", ] [[package]] @@ -687,6 +719,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + [[package]] name = "terminal_size" version = "0.1.15" @@ -706,6 +747,15 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thread_local" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" +dependencies = [ + "lazy_static", +] + [[package]] name = "tokio" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index b6c8998..45fbb41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ edition = "2018" async-trait = "0.1.42" clap = "2.33.3" console = "0.13.0" +env_logger = "0.8.2" futures = "0.3.8" glob = "0.3.0" hostname = "0.3.1" diff --git a/default.nix b/default.nix index e0be259..a8fe355 100644 --- a/default.nix +++ b/default.nix @@ -10,5 +10,5 @@ in rustPlatform.buildRustPackage { version = "0.1.0"; src = ./.; - cargoSha256 = "0gwjbzvx6hlbjb8892rc2p9rj5l432y13aq1nxr2h71rgqppxflg"; + cargoSha256 = "1ai046vbvydyqhwiy8qz0d28dch5jpxg3rzk7nrh2sdwcvxirmvm"; } diff --git a/src/command/apply.rs b/src/command/apply.rs index 58bdc82..2fe21c8 100644 --- a/src/command/apply.rs +++ b/src/command/apply.rs @@ -1,6 +1,6 @@ use clap::{Arg, App, SubCommand, ArgMatches}; -use crate::nix::{Hive, DeploymentTask, DeploymentGoal}; +use crate::nix::{DeploymentTask, DeploymentGoal}; use crate::deployment::deploy; use crate::util; @@ -37,9 +37,9 @@ pub fn subcommand() -> App<'static, 'static> { } pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) { - let mut hive = Hive::from_args(local_args).unwrap(); + let mut hive = util::hive_from_args(local_args).unwrap(); - println!("Enumerating nodes..."); + log::info!("Enumerating nodes..."); let all_nodes = hive.deployment_info().await.unwrap(); let selected_nodes = match local_args.value_of("on") { @@ -50,14 +50,14 @@ pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) { }; if selected_nodes.len() == 0 { - println!("No hosts matched. Exiting..."); + log::warn!("No hosts matched. Exiting..."); quit::with_code(2); } if selected_nodes.len() == all_nodes.len() { - println!("Building all node configurations..."); + log::info!("Building all node configurations..."); } else { - println!("Selected {} out of {} hosts. Building node configurations...", selected_nodes.len(), all_nodes.len()); + log::info!("Selected {} out of {} hosts. Building node configurations...", selected_nodes.len(), all_nodes.len()); } // Some ugly argument mangling :/ @@ -88,9 +88,9 @@ pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) { } if skip_list.len() != 0 { - println!("Applying configurations ({} skipped)...", skip_list.len()); + log::info!("Applying configurations ({} skipped)...", skip_list.len()); } else { - println!("Applying configurations..."); + log::info!("Applying configurations..."); } deploy(task_list, max_parallelism, !verbose).await; diff --git a/src/command/apply_local.rs b/src/command/apply_local.rs index 9d18062..524cc0e 100644 --- a/src/command/apply_local.rs +++ b/src/command/apply_local.rs @@ -4,12 +4,12 @@ use clap::{Arg, App, SubCommand, ArgMatches}; use tokio::fs; use tokio::process::Command; -use crate::nix::{Hive, DeploymentTask, DeploymentGoal, Host}; +use crate::nix::{DeploymentTask, DeploymentGoal, Host}; use crate::nix::host; use crate::util; pub fn subcommand() -> App<'static, 'static> { - let command = SubCommand::with_name("apply-local") + SubCommand::with_name("apply-local") .about("Apply configurations on the local machine") .arg(Arg::with_name("goal") .help("Deployment goal") @@ -17,12 +17,6 @@ pub fn subcommand() -> App<'static, 'static> { .default_value("switch") .index(1) .possible_values(&["push", "switch", "boot", "test", "dry-activate"])) - .arg(Arg::with_name("config") - .short("f") - .long("config") - .help("Path to a Hive expression") - .default_value("hive.nix") - .required(true)) .arg(Arg::with_name("sudo") .long("sudo") .help("Attempt to escalate privileges if not run as root") @@ -30,20 +24,18 @@ pub fn subcommand() -> App<'static, 'static> { .arg(Arg::with_name("we-are-launched-by-sudo") .long("we-are-launched-by-sudo") .hidden(true) - .takes_value(false)); - - util::register_common_args(command) + .takes_value(false)) } pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) { // Sanity check: Are we running NixOS? if let Ok(os_release) = fs::read_to_string("/etc/os-release").await { if !os_release.contains("ID=nixos\n") { - eprintln!("\"apply-local\" only works on NixOS machines."); + log::error!("\"apply-local\" only works on NixOS machines."); quit::with_code(5); } } else { - eprintln!("Coult not detect the OS version from /etc/os-release."); + log::error!("Coult not detect the OS version from /etc/os-release."); quit::with_code(5); } @@ -52,42 +44,42 @@ pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) { let euid: u32 = unsafe { libc::geteuid() }; if euid != 0 { if local_args.is_present("we-are-launched-by-sudo") { - eprintln!("Failed to escalate privileges. We are still not root despite a successful sudo invocation."); + log::error!("Failed to escalate privileges. We are still not root despite a successful sudo invocation."); quit::with_code(3); } if local_args.is_present("sudo") { escalate().await; } else { - eprintln!("Colmena was not started by root. This is probably not going to work."); - eprintln!("Hint: Add the --sudo flag."); + log::warn!("Colmena was not started by root. This is probably not going to work."); + log::warn!("Hint: Add the --sudo flag."); } } } - let mut hive = Hive::from_args(local_args).unwrap(); + let mut hive = util::hive_from_args(local_args).unwrap(); let hostname = hostname::get().expect("Could not get hostname") .to_string_lossy().into_owned(); let goal = DeploymentGoal::from_str(local_args.value_of("goal").unwrap()).unwrap(); - println!("Enumerating nodes..."); + log::info!("Enumerating nodes..."); let all_nodes = hive.deployment_info().await.unwrap(); let target: Box = { if let Some(info) = all_nodes.get(&hostname) { if !info.allows_local_deployment() { - eprintln!("Local deployment is not enabled for host {}.", hostname); - eprintln!("Hint: Set deployment.allowLocalDeployment to true."); + log::error!("Local deployment is not enabled for host {}.", hostname); + log::error!("Hint: Set deployment.allowLocalDeployment to true."); quit::with_code(2); } host::local() } else { - eprintln!("Host {} is not present in the Hive configuration.", hostname); + log::error!("Host {} is not present in the Hive configuration.", hostname); quit::with_code(2); } }; - println!("Building local node configuration..."); + log::info!("Building local node configuration..."); let profile = { let selected_nodes: Vec = vec![hostname.clone()]; let mut profiles = hive.build_selected(selected_nodes).await diff --git a/src/command/build.rs b/src/command/build.rs index a2fca67..a59593a 100644 --- a/src/command/build.rs +++ b/src/command/build.rs @@ -1,6 +1,5 @@ use clap::{Arg, App, SubCommand, ArgMatches}; -use crate::nix::Hive; use crate::util; pub fn subcommand() -> App<'static, 'static> { @@ -17,9 +16,9 @@ pub fn subcommand() -> App<'static, 'static> { } pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) { - let mut hive = Hive::from_args(local_args).unwrap(); + let mut hive = util::hive_from_args(local_args).unwrap(); - println!("Enumerating nodes..."); + log::info!("Enumerating nodes..."); let all_nodes = hive.deployment_info().await.unwrap(); let selected_nodes = match local_args.value_of("on") { @@ -30,17 +29,17 @@ pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) { }; if selected_nodes.len() == 0 { - println!("No hosts matched. Exiting..."); + log::warn!("No hosts matched. Exiting..."); quit::with_code(2); } if selected_nodes.len() == all_nodes.len() { - println!("Building all node configurations..."); + log::info!("Building all node configurations..."); } else { - println!("Selected {} out of {} hosts. Building node configurations...", selected_nodes.len(), all_nodes.len()); + log::info!("Selected {} out of {} hosts. Building node configurations...", selected_nodes.len(), all_nodes.len()); } hive.build_selected(selected_nodes).await.unwrap(); - println!("Success!"); + log::info!("Success!"); } diff --git a/src/command/introspect.rs b/src/command/introspect.rs index 3614ee8..9a5f98b 100644 --- a/src/command/introspect.rs +++ b/src/command/introspect.rs @@ -2,11 +2,10 @@ use std::path::PathBuf; use clap::{Arg, App, SubCommand, ArgMatches}; -use crate::nix::Hive; use crate::util; pub fn subcommand() -> App<'static, 'static> { - let command = SubCommand::with_name("introspect") + SubCommand::with_name("introspect") .about("Evaluate expressions using the complete configuration.") .long_about(r#"Your expression should take an attribute set with keys `pkgs`, `lib` and `nodes` (like a NixOS module) and return a JSON-serializable value. @@ -22,22 +21,13 @@ For example, to retrieve the configuration of one node, you may write something .short("E") .help("The Nix expression") .takes_value(true)) - .arg(Arg::with_name("config") - .short("f") - .long("config") - .help("Path to a Hive expression") - .default_value("hive.nix") - .required(true)) - ; - - util::register_common_args(command) } pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) { - let mut hive = Hive::from_args(local_args).unwrap(); + let mut hive = util::hive_from_args(local_args).unwrap(); if !(local_args.is_present("expression") ^ local_args.is_present("expression_file")) { - eprintln!("Either an expression (-E) xor a .nix file containing an expression should be specified, not both."); + log::error!("Either an expression (-E) xor a .nix file containing an expression should be specified, not both."); quit::with_code(1); } diff --git a/src/main.rs b/src/main.rs index e99d91b..fcf4aa9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ -use clap::{App, AppSettings}; +use std::env; +use clap::{App, AppSettings, Arg}; mod nix; mod command; @@ -29,22 +30,60 @@ macro_rules! bind_command { #[tokio::main(flavor = "multi_thread")] async fn main() { + init_logging(); + let mut app = App::new("Colmena") .version("0.1.0") .author("Zhaofeng Li ") .about("NixOS deployment tool") .global_setting(AppSettings::ColoredHelp) - .setting(AppSettings::ArgRequiredElseHelp); + .setting(AppSettings::ArgRequiredElseHelp) + .arg(Arg::with_name("config") + .short("f") + .long("config") + .help("Path to a Hive expression") + + // The default value is a lie (sort of)! + // + // The default behavior is to search upwards from the + // current working directory for a file named "hive.nix". + // This behavior is disabled if --config/-f is explicitly + // supplied by the user (occurrences_of > 0). + .default_value("hive.nix") + .long_help(r#"If this argument is not specified, Colmena will search upwards from the current working directory for a file named "hive.nix". This behavior is disabled if --config/-f is given explicitly. + +For a sample configuration, see . +"#) + .global(true)) + .arg(Arg::with_name("show-trace") + .long("show-trace") + .help("Show debug information for Nix commands") + .long_help("Passes --show-trace to Nix commands") + .global(true) + .takes_value(false)); bind_command!(apply, app); bind_command!(apply_local, app); bind_command!(build, app); bind_command!(introspect, app); - let matches = app.get_matches(); + let matches = app.clone().get_matches(); command!(apply, matches); command!("apply-local", apply_local, matches); command!(build, matches); command!(introspect, matches); + + app.print_long_help().unwrap(); +} + +fn init_logging() { + if env::var("RUST_LOG").is_err() { + // HACK + env::set_var("RUST_LOG", "info") + } + env_logger::builder() + .format_timestamp(None) + .format_module_path(false) + .init(); } diff --git a/src/nix/host.rs b/src/nix/host.rs index 0590ef8..fb9cfde 100644 --- a/src/nix/host.rs +++ b/src/nix/host.rs @@ -187,7 +187,7 @@ impl SSH { progress.set_message(trimmed); progress.inc(0); } else { - println!("{} | {}", style(&self.friendly_name).cyan(), trimmed); + eprintln!("{} | {}", style(&self.friendly_name).cyan(), trimmed); } self.logs.push(line); } diff --git a/src/nix/mod.rs b/src/nix/mod.rs index ad9e7ee..927ebea 100644 --- a/src/nix/mod.rs +++ b/src/nix/mod.rs @@ -6,7 +6,6 @@ use std::collections::HashMap; use std::fs; use async_trait::async_trait; -use clap::ArgMatches; use indicatif::ProgressBar; use serde::de::DeserializeOwned; use serde::{Serialize, Deserialize}; @@ -72,17 +71,8 @@ impl Hive { }) } - pub fn from_args(args: &ArgMatches<'_>) -> NixResult { - let path = args.value_of("config").expect("The config arg should exist").to_owned(); - let path = canonicalize_path(path); - - let mut hive = Self::new(path)?; - - if args.is_present("show-trace") { - hive.show_trace = true; - } - - Ok(hive) + pub fn show_trace(&mut self, value: bool) { + self.show_trace = value; } /// Retrieve deployment info for all nodes @@ -443,12 +433,3 @@ impl DeploymentTask { } } } - -fn canonicalize_path(path: String) -> PathBuf { - if !path.starts_with("/") { - format!("./{}", path).into() - } else { - path.into() - } -} - diff --git a/src/util.rs b/src/util.rs index ba09b89..19ff9f2 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,15 +1,89 @@ use std::collections::HashMap; +use std::path::PathBuf; +use std::fs; -use clap::{Arg, App}; +use clap::{App, Arg, ArgMatches}; use glob::Pattern as GlobPattern; -use super::nix::DeploymentConfig; +use super::nix::{DeploymentConfig, Hive, NixResult}; enum NodeFilter { NameFilter(GlobPattern), TagFilter(GlobPattern), } +pub fn hive_from_args(args: &ArgMatches<'_>) -> NixResult { + let path = match args.occurrences_of("config") { + 0 => { + // traverse upwards until we find hive.nix + let mut cur = std::env::current_dir()?; + let mut hive_path = None; + + loop { + let mut listing = match fs::read_dir(&cur) { + Ok(listing) => listing, + Err(e) => { + // This can very likely fail in shared environments + // where users aren't able to list /home. It's not + // unexpected. + // + // It may not be immediately obvious to the user that + // we are traversing upwards to find hive.nix. + log::warn!("Could not traverse up ({:?}) to find hive.nix: {}", cur, e); + break; + }, + }; + + let found = listing.find_map(|rdirent| { + match rdirent { + Err(e) => Some(Err(e)), + Ok(f) => { + if f.file_name() == "hive.nix" { + Some(Ok(f)) + } else { + None + } + } + } + }); + + if let Some(rdirent) = found { + let dirent = rdirent?; + hive_path = Some(dirent.path()); + break; + } + + match cur.parent() { + Some(parent) => { + cur = parent.to_owned(); + } + None => { + break; + } + } + } + + if hive_path.is_none() { + log::error!("Could not find `hive.nix` in {:?} or any parent directory", std::env::current_dir()?); + } + + hive_path.unwrap() + } + _ => { + let path = args.value_of("config").expect("The config arg should exist").to_owned(); + canonicalize_cli_path(path) + } + }; + + let mut hive = Hive::new(path)?; + + if args.is_present("show-trace") { + hive.show_trace(true); + } + + Ok(hive) +} + pub fn filter_nodes(nodes: &HashMap, filter: &str) -> Vec { let filters: Vec = filter.split(",").map(|pattern| { use NodeFilter::*; @@ -48,24 +122,7 @@ pub fn filter_nodes(nodes: &HashMap, filter: &str) -> } } -pub fn register_common_args<'a, 'b>(command: App<'a, 'b>) -> App<'a, 'b> { - command - .arg(Arg::with_name("config") - .short("f") - .long("config") - .help("Path to a Hive expression") - .default_value("hive.nix") - .required(true)) - .arg(Arg::with_name("show-trace") - .long("show-trace") - .help("Show debug information for Nix commands") - .long_help("Passes --show-trace to Nix commands") - .takes_value(false)) -} - pub fn register_selector_args<'a, 'b>(command: App<'a, 'b>) -> App<'a, 'b> { - let command = register_common_args(command); - command .arg(Arg::with_name("on") .long("on") @@ -79,3 +136,11 @@ Valid examples: - @a-tag,@tags-can-have-*"#) .takes_value(true)) } + +fn canonicalize_cli_path(path: String) -> PathBuf { + if !path.starts_with("/") { + format!("./{}", path).into() + } else { + path.into() + } +}