forked from DGNum/colmena
commit
3538f18b30
21 changed files with 696 additions and 804 deletions
33
Cargo.lock
generated
33
Cargo.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
390
src/cli.rs
390
src/cli.rs
|
@ -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");
|
||||||
#[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);
|
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
use crate::troubleshooter::run_wrapped as r;
|
||||||
handle_command!("test-progress", test_progress, matches);
|
|
||||||
|
|
||||||
if let Some(args) = matches.subcommand_matches("gen-completions") {
|
match opts.command {
|
||||||
return gen_completions(args);
|
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 {
|
||||||
|
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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(),);
|
||||||
|
|
||||||
|
|
|
@ -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 = {
|
hostname::get()
|
||||||
let s = if local_args.contains_id("node") {
|
.expect("Could not get hostname")
|
||||||
local_args.get_one::<String>("node").unwrap().to_owned()
|
.to_string_lossy()
|
||||||
} else {
|
.into_owned()
|
||||||
hostname::get()
|
}))?;
|
||||||
.expect("Could not get hostname")
|
|
||||||
.to_string_lossy()
|
|
||||||
.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?;
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -1,80 +1,62 @@
|
||||||
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,
|
||||||
|
) -> Result<(), ColmenaError> {
|
||||||
|
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 hive = util::hive_from_args(local_args).await?;
|
let Some(expression) = expression else {
|
||||||
|
log::error!("Provide either an expression (-E) or a .nix file containing an expression.");
|
||||||
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);
|
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!(
|
|
||||||
"import {}",
|
|
||||||
path.canonicalize()
|
|
||||||
.expect("Could not generate absolute path to expression file.")
|
|
||||||
.to_str()
|
|
||||||
.unwrap()
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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 {
|
||||||
|
|
|
@ -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({
|
} else {
|
||||||
let limit = local_args.get_one::<usize>("parallel").unwrap().to_owned();
|
None
|
||||||
|
|
||||||
if limit > 0 {
|
|
||||||
Some(Semaphore::new(limit))
|
|
||||||
} else {
|
|
||||||
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();
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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());
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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}"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
122
src/util.rs
122
src/util.rs
|
@ -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>,
|
||||||
|
|
Loading…
Reference in a new issue