From d01e9404b8ff33daf0cc8226fcae99adcf92e305 Mon Sep 17 00:00:00 2001 From: Kasper Seweryn Date: Fri, 11 Jul 2025 13:58:41 +0200 Subject: [PATCH] feat: add `--remote-bin-path` option (#298) --- interface.json | 3 ++ src/bin/activate.rs | 110 ++++++++++++++++++++++++++++++-------------- src/cli.rs | 79 +++++++++++++++++++------------ src/data.rs | 4 +- src/deploy.rs | 96 +++++++++++++++++++++++++++----------- src/lib.rs | 22 +++++++-- src/push.rs | 89 +++++++++++++++++++++++++++-------- 7 files changed, 287 insertions(+), 116 deletions(-) diff --git a/interface.json b/interface.json index a96d1c2d..d68aa62b 100644 --- a/interface.json +++ b/interface.json @@ -38,6 +38,9 @@ }, "interactiveSudo": { "type": "boolean" + }, + "remoteBinPath": { + "type": "string" } } }, diff --git a/src/bin/activate.rs b/src/bin/activate.rs index 9199e791..e163ea22 100644 --- a/src/bin/activate.rs +++ b/src/bin/activate.rs @@ -90,6 +90,10 @@ struct ActivateOpts { /// Path for any temporary files that may be needed during activation #[arg(long)] temp_path: PathBuf, + + /// Path to where the nix-store and nix-env binaries are stored + #[arg(long)] + bin_path: Option, } /// Wait for profile activation @@ -119,6 +123,10 @@ struct RevokeOpts { /// The profile name #[arg(long, requires = "profile_user")] profile_name: Option, + + /// Path to where the nix-store and nix-env binaries are stored + #[arg(long)] + bin_path: Option, } #[derive(Error, Debug)] @@ -143,10 +151,26 @@ pub enum DeactivateError { ReactivateExit(Option), } -pub async fn deactivate(profile_path: &str) -> Result<(), DeactivateError> { +fn create_command(program: impl Into, bin_path: &Option) -> Command { + let mut command = Command::new(program.into()); + + if let (Some(bin_path), Ok(path_env)) = (bin_path, std::env::var("PATH")) { + command.env( + "PATH", + format!("{}:{}", bin_path.to_string_lossy(), path_env), + ); + } + + command +} + +pub async fn deactivate( + profile_path: &str, + bin_path: Option, +) -> Result<(), DeactivateError> { warn!("De-activating due to error"); - let nix_env_rollback_exit_status = Command::new("nix-env") + let nix_env_rollback_exit_status = create_command("nix-env", &bin_path) .arg("-p") .arg(&profile_path) .arg("--rollback") @@ -161,7 +185,7 @@ pub async fn deactivate(profile_path: &str) -> Result<(), DeactivateError> { debug!("Listing generations"); - let nix_env_list_generations_out = Command::new("nix-env") + let nix_env_list_generations_out = create_command("nix-env", &bin_path) .arg("-p") .arg(&profile_path) .arg("--list-generations") @@ -190,7 +214,7 @@ pub async fn deactivate(profile_path: &str) -> Result<(), DeactivateError> { debug!("Removing generation entry {}", last_generation_line); warn!("Removing generation by ID {}", last_generation_id); - let nix_env_delete_generation_exit_status = Command::new("nix-env") + let nix_env_delete_generation_exit_status = create_command("nix-env", &bin_path) .arg("-p") .arg(&profile_path) .arg("--delete-generations") @@ -206,12 +230,13 @@ pub async fn deactivate(profile_path: &str) -> Result<(), DeactivateError> { info!("Attempting to re-activate the last generation"); - let re_activate_exit_status = Command::new(format!("{}/deploy-rs-activate", profile_path)) - .env("PROFILE", &profile_path) - .current_dir(&profile_path) - .status() - .await - .map_err(DeactivateError::Reactivate)?; + let re_activate_exit_status = + create_command(format!("{}/deploy-rs-activate", profile_path), &bin_path) + .env("PROFILE", &profile_path) + .current_dir(&profile_path) + .status() + .await + .map_err(DeactivateError::Reactivate)?; match re_activate_exit_status.code() { Some(0) => (), @@ -315,7 +340,11 @@ pub enum WaitError { #[error("Error waiting for activation: {0}")] Waiting(#[from] DangerZoneError), } -pub async fn wait(temp_path: PathBuf, closure: String, activation_timeout: Option) -> Result<(), WaitError> { +pub async fn wait( + temp_path: PathBuf, + closure: String, + activation_timeout: Option, +) -> Result<(), WaitError> { let lock_path = deploy::make_lock_path(&temp_path, &closure); let (created, done) = mpsc::channel(1); @@ -386,6 +415,7 @@ pub async fn activate( closure: String, auto_rollback: bool, temp_path: PathBuf, + bin_path: Option, confirm_timeout: u16, magic_rollback: bool, dry_activate: bool, @@ -393,7 +423,8 @@ pub async fn activate( ) -> Result<(), ActivateError> { if !dry_activate { info!("Activating profile"); - let nix_env_set_exit_status = Command::new("nix-env") + + let nix_env_set_exit_status = create_command("nix-env", &bin_path) .arg("-p") .arg(&profile_path) .arg("--set") @@ -405,7 +436,7 @@ pub async fn activate( Some(0) => (), a => { if auto_rollback && !dry_activate { - deactivate(&profile_path).await?; + deactivate(&profile_path, bin_path).await?; } return Err(ActivateError::SetProfileExit(a)); } @@ -420,19 +451,22 @@ pub async fn activate( &profile_path }; - let activate_status = match Command::new(format!("{}/deploy-rs-activate", activation_location)) - .env("PROFILE", activation_location) - .env("DRY_ACTIVATE", if dry_activate { "1" } else { "0" }) - .env("BOOT", if boot { "1" } else { "0" }) - .current_dir(activation_location) - .status() - .await - .map_err(ActivateError::RunActivate) + let activate_status = match create_command( + format!("{}/deploy-rs-activate", activation_location), + &bin_path, + ) + .env("PROFILE", activation_location) + .env("DRY_ACTIVATE", if dry_activate { "1" } else { "0" }) + .env("BOOT", if boot { "1" } else { "0" }) + .current_dir(activation_location) + .status() + .await + .map_err(ActivateError::RunActivate) { Ok(x) => x, Err(e) => { if auto_rollback && !dry_activate { - deactivate(&profile_path).await?; + deactivate(&profile_path, bin_path).await?; } return Err(e); } @@ -443,7 +477,7 @@ pub async fn activate( Some(0) => (), a => { if auto_rollback { - deactivate(&profile_path).await?; + deactivate(&profile_path, bin_path).await?; } return Err(ActivateError::RunActivateExit(a)); } @@ -456,7 +490,7 @@ pub async fn activate( if magic_rollback && !boot { info!("Magic rollback is enabled, setting up confirmation hook..."); if let Err(err) = activation_confirmation(temp_path, confirm_timeout, closure).await { - deactivate(&profile_path).await?; + deactivate(&profile_path, bin_path).await?; return Err(ActivateError::ActivationConfirmation(err)); } } @@ -465,8 +499,8 @@ pub async fn activate( Ok(()) } -async fn revoke(profile_path: String) -> Result<(), DeactivateError> { - deactivate(profile_path.as_str()).await?; +async fn revoke(profile_path: String, bin_path: Option) -> Result<(), DeactivateError> { + deactivate(profile_path.as_str(), bin_path).await?; Ok(()) } @@ -557,6 +591,7 @@ async fn main() -> Result<(), Box> { activate_opts.closure, activate_opts.auto_rollback, activate_opts.temp_path, + activate_opts.bin_path, activate_opts.confirm_timeout, activate_opts.magic_rollback, activate_opts.dry_activate, @@ -565,15 +600,22 @@ async fn main() -> Result<(), Box> { .await .map_err(|x| Box::new(x) as Box), - SubCommand::Wait(wait_opts) => wait(wait_opts.temp_path, wait_opts.closure, wait_opts.activation_timeout) - .await - .map_err(|x| Box::new(x) as Box), + SubCommand::Wait(wait_opts) => wait( + wait_opts.temp_path, + wait_opts.closure, + wait_opts.activation_timeout, + ) + .await + .map_err(|x| Box::new(x) as Box), - SubCommand::Revoke(revoke_opts) => revoke(get_profile_path( - revoke_opts.profile_path, - revoke_opts.profile_user, - revoke_opts.profile_name, - )?) + SubCommand::Revoke(revoke_opts) => revoke( + get_profile_path( + revoke_opts.profile_path, + revoke_opts.profile_user, + revoke_opts.profile_name, + )?, + revoke_opts.bin_path, + ) .await .map_err(|x| Box::new(x) as Box), }; diff --git a/src/cli.rs b/src/cli.rs index 43990f5c..920fc504 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -6,7 +6,7 @@ use std::collections::HashMap; use std::io::{stdin, stdout, Write}; -use clap::{ArgMatches, Parser, FromArgMatches}; +use clap::{ArgMatches, FromArgMatches, Parser}; use crate as deploy; @@ -109,6 +109,9 @@ pub struct Opts { /// Prompt for sudo password during activation. #[arg(long)] interactive_sudo: Option, + /// Path to where the nix-store and nix-env binaries are stored on the remote host + #[arg(long)] + remote_bin_path: Option, } /// Returns if the available Nix installation supports flakes @@ -386,9 +389,9 @@ pub enum RunDeployError { #[error("Failed to deploy profile to node {0}: {1}")] DeployProfile(String, deploy::deploy::DeployProfileError), #[error("Failed to build profile on node {0}: {0}")] - BuildProfile(String, deploy::push::PushProfileError), + BuildProfile(String, deploy::push::PushProfileError), #[error("Failed to push profile to node {0}: {0}")] - PushProfile(String, deploy::push::PushProfileError), + PushProfile(String, deploy::push::PushProfileError), #[error("No profile named `{0}` was found")] ProfileNotFound(String), #[error("No node named `{0}` was found")] @@ -404,7 +407,7 @@ pub enum RunDeployError { #[error("Failed to revoke profile for node {0}: {1}")] RevokeProfile(String, deploy::deploy::RevokeProfileError), #[error("Deployment to node {0} failed, rolled back to previous generation")] - Rollback(String) + Rollback(String), } type ToDeploy<'a> = Vec<( @@ -548,7 +551,11 @@ async fn run_deploy( let mut deploy_defs = deploy_data.defs()?; - if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) { + if deploy_data + .merged_settings + .interactive_sudo + .unwrap_or(false) + { warn!("Interactive sudo is enabled! Using a sudo password is less secure than correctly configured SSH keys.\nPlease use keys in production environments."); if deploy_data.merged_settings.sudo.is_some() { @@ -560,8 +567,15 @@ async fn run_deploy( deploy_defs.sudo = Some(format!("{} -S -p \"\"", original)); } - info!("You will now be prompted for the sudo password for {}.", node.node_settings.hostname); - let sudo_password = rpassword::prompt_password(format!("(sudo for {}) Password: ", node.node_settings.hostname)).unwrap_or("".to_string()); + info!( + "You will now be prompted for the sudo password for {}.", + node.node_settings.hostname + ); + let sudo_password = rpassword::prompt_password(format!( + "(sudo for {}) Password: ", + node.node_settings.hostname + )) + .unwrap_or("".to_string()); deploy_defs.sudo_password = Some(sudo_password); } @@ -592,16 +606,16 @@ async fn run_deploy( for data in data_iter() { let node_name: String = data.deploy_data.node_name.to_string(); - deploy::push::build_profile(data).await.map_err(|e| { - RunDeployError::BuildProfile(node_name, e) - })?; + deploy::push::build_profile(data) + .await + .map_err(|e| RunDeployError::BuildProfile(node_name, e))?; } for data in data_iter() { let node_name: String = data.deploy_data.node_name.to_string(); - deploy::push::push_profile(data).await.map_err(|e| { - RunDeployError::PushProfile(node_name, e) - })?; + deploy::push::push_profile(data) + .await + .map_err(|e| RunDeployError::PushProfile(node_name, e))?; } let mut succeeded: Vec<(&deploy::DeployData, &deploy::DeployDefs)> = vec![]; @@ -611,7 +625,8 @@ async fn run_deploy( // Rollbacks adhere to the global seeting to auto_rollback and secondary // the profile's configuration for (_, deploy_data, deploy_defs) in &parts { - if let Err(e) = deploy::deploy::deploy_profile(deploy_data, deploy_defs, dry_activate, boot).await + if let Err(e) = + deploy::deploy::deploy_profile(deploy_data, deploy_defs, dry_activate, boot).await { error!("{}", e); if dry_activate { @@ -624,14 +639,19 @@ async fn run_deploy( // the command line) for (deploy_data, deploy_defs) in &succeeded { if deploy_data.merged_settings.auto_rollback.unwrap_or(true) { - deploy::deploy::revoke(*deploy_data, *deploy_defs).await.map_err(|e| { - RunDeployError::RevokeProfile(deploy_data.node_name.to_string(), e) - })?; + deploy::deploy::revoke(*deploy_data, *deploy_defs) + .await + .map_err(|e| { + RunDeployError::RevokeProfile(deploy_data.node_name.to_string(), e) + })?; } } return Err(RunDeployError::Rollback(deploy_data.node_name.to_string())); } - return Err(RunDeployError::DeployProfile(deploy_data.node_name.to_string(), e)) + return Err(RunDeployError::DeployProfile( + deploy_data.node_name.to_string(), + e, + )); } succeeded.push((deploy_data, deploy_defs)) } @@ -682,18 +702,16 @@ pub async fn run(args: Option<&ArgMatches>) -> Result<(), RunError> { .targets .unwrap_or_else(|| vec![opts.clone().target.unwrap_or_else(|| ".".to_string())]); - let deploy_flakes: Vec = - if let Some(file) = &opts.file { - deploys - .iter() - .map(|f| deploy::parse_file(file.as_str(), f.as_str())) - .collect::, ParseFlakeError>>()? - } - else { + let deploy_flakes: Vec = if let Some(file) = &opts.file { deploys - .iter() - .map(|f| deploy::parse_flake(f.as_str())) - .collect::, ParseFlakeError>>()? + .iter() + .map(|f| deploy::parse_file(file.as_str(), f.as_str())) + .collect::, ParseFlakeError>>()? + } else { + deploys + .iter() + .map(|f| deploy::parse_flake(f.as_str())) + .collect::, ParseFlakeError>>()? }; let cmd_overrides = deploy::CmdOverrides { @@ -710,7 +728,8 @@ pub async fn run(args: Option<&ArgMatches>) -> Result<(), RunError> { dry_activate: opts.dry_activate, remote_build: opts.remote_build, sudo: opts.sudo, - interactive_sudo: opts.interactive_sudo + interactive_sudo: opts.interactive_sudo, + remote_bin_path: opts.remote_bin_path, }; let supports_flakes = test_flake_support().await.map_err(RunError::FlakeTest)?; diff --git a/src/data.rs b/src/data.rs index 12b0f01b..15104f50 100644 --- a/src/data.rs +++ b/src/data.rs @@ -33,10 +33,12 @@ pub struct GenericSettings { pub magic_rollback: Option, #[serde(rename(deserialize = "sudo"))] pub sudo: Option, - #[serde(default,rename(deserialize = "remoteBuild"))] + #[serde(default, rename(deserialize = "remoteBuild"))] pub remote_build: Option, #[serde(rename(deserialize = "interactiveSudo"))] pub interactive_sudo: Option, + #[serde(default, rename(deserialize = "remoteBinPath"))] + pub remote_bin_path: Option, } #[derive(Deserialize, Debug, Clone)] diff --git a/src/deploy.rs b/src/deploy.rs index fd535443..41821e11 100644 --- a/src/deploy.rs +++ b/src/deploy.rs @@ -17,6 +17,7 @@ struct ActivateCommandData<'a> { closure: &'a str, auto_rollback: bool, temp_path: &'a Path, + bin_path: Option<&'a Path>, confirm_timeout: u16, magic_rollback: bool, debug_logs: bool, @@ -75,6 +76,14 @@ fn build_activate_command(data: &ActivateCommandData) -> String { self_activate_command = format!("{} --boot", self_activate_command); } + if let Some(path) = &data.bin_path { + self_activate_command = format!( + "{} --bin-path {}", + self_activate_command, + path.to_string_lossy() + ); + } + if let Some(sudo_cmd) = &data.sudo { self_activate_command = format!("{} {}", sudo_cmd, self_activate_command); } @@ -93,6 +102,7 @@ fn test_activation_command_builder() { let dry_activate = false; let boot = false; let temp_path = Path::new("/tmp"); + let bin_path = Some(Path::new("/nix/var/nix/profiles/default/bin")); let confirm_timeout = 30; let magic_rollback = true; let debug_logs = true; @@ -105,6 +115,7 @@ fn test_activation_command_builder() { closure, auto_rollback, temp_path, + bin_path, confirm_timeout, magic_rollback, debug_logs, @@ -112,7 +123,7 @@ fn test_activation_command_builder() { dry_activate, boot, }), - "sudo -u test /nix/store/blah/etc/activate-rs --debug-logs --log-dir /tmp/something.txt activate '/nix/store/blah/etc' --profile-path '/blah/profiles/test' --temp-path '/tmp' --confirm-timeout 30 --magic-rollback --auto-rollback" + "sudo -u test /nix/store/blah/etc/activate-rs --debug-logs --log-dir /tmp/something.txt activate '/nix/store/blah/etc' --profile-path '/blah/profiles/test' --temp-path '/tmp' --confirm-timeout 30 --magic-rollback --auto-rollback --bin-path /nix/var/nix/profiles/default/bin" .to_string(), ); } @@ -144,7 +155,10 @@ fn build_wait_command(data: &WaitCommandData) -> String { data.temp_path.display(), ); if let Some(activation_timeout) = data.activation_timeout { - self_activate_command = format!("{} --activation-timeout {}", self_activate_command, activation_timeout); + self_activate_command = format!( + "{} --activation-timeout {}", + self_activate_command, activation_timeout + ); } if let Some(sudo_cmd) = &data.sudo { @@ -242,20 +256,27 @@ fn test_revoke_command_builder() { ); } -async fn handle_sudo_stdin(ssh_activate_child: &mut tokio::process::Child, deploy_defs: &DeployDefs) -> Result<(), std::io::Error> { +async fn handle_sudo_stdin( + ssh_activate_child: &mut tokio::process::Child, + deploy_defs: &DeployDefs, +) -> Result<(), std::io::Error> { match ssh_activate_child.stdin.as_mut() { Some(stdin) => { - let _ = stdin.write_all(format!("{}\n",deploy_defs.sudo_password.clone().unwrap_or("".to_string())).as_bytes()).await; - Ok(()) - } - None => { - Err( - std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to open stdin for sudo command", + let _ = stdin + .write_all( + format!( + "{}\n", + deploy_defs.sudo_password.clone().unwrap_or("".to_string()) + ) + .as_bytes(), ) - ) + .await; + Ok(()) } + None => Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Failed to open stdin for sudo command", + )), } } @@ -300,8 +321,12 @@ pub async fn confirm_profile( .arg(confirm_command) .spawn() .map_err(ConfirmProfileError::SSHConfirm)?; - - if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) { + + if deploy_data + .merged_settings + .interactive_sudo + .unwrap_or(false) + { trace!("[confirm] Piping in sudo password"); handle_sudo_stdin(&mut ssh_confirm_child, deploy_defs) .await @@ -311,7 +336,7 @@ pub async fn confirm_profile( let ssh_confirm_exit_status = ssh_confirm_child .wait() .await - .map_err(ConfirmProfileError::SSHConfirm)?; + .map_err(ConfirmProfileError::SSHConfirm)?; match ssh_confirm_exit_status.code() { Some(0) => (), @@ -380,7 +405,8 @@ pub async fn deploy_profile( profile_info: &deploy_data.get_profile_info()?, closure: &deploy_data.profile.profile_settings.path, auto_rollback, - temp_path: temp_path, + temp_path, + bin_path: deploy_data.merged_settings.remote_bin_path.as_deref(), confirm_timeout, magic_rollback, debug_logs: deploy_data.debug_logs, @@ -404,7 +430,7 @@ pub async fn deploy_profile( .stdin(std::process::Stdio::piped()); for ssh_opt in &deploy_data.merged_settings.ssh_opts { - ssh_activate_command.arg(&ssh_opt); + ssh_activate_command.arg(ssh_opt); } if !magic_rollback || dry_activate || boot { @@ -413,7 +439,11 @@ pub async fn deploy_profile( .spawn() .map_err(DeployProfileError::SSHSpawnActivate)?; - if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) { + if deploy_data + .merged_settings + .interactive_sudo + .unwrap_or(false) + { trace!("[activate] Piping in sudo password"); handle_sudo_stdin(&mut ssh_activate_child, deploy_defs) .await @@ -441,8 +471,8 @@ pub async fn deploy_profile( let self_wait_command = build_wait_command(&WaitCommandData { sudo: &deploy_defs.sudo, closure: &deploy_data.profile.profile_settings.path, - temp_path: temp_path, - activation_timeout: activation_timeout, + temp_path, + activation_timeout, debug_logs: deploy_data.debug_logs, log_dir: deploy_data.log_dir, }); @@ -454,7 +484,11 @@ pub async fn deploy_profile( .spawn() .map_err(DeployProfileError::SSHSpawnActivate)?; - if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) { + if deploy_data + .merged_settings + .interactive_sudo + .unwrap_or(false) + { trace!("[activate] Piping in sudo password"); handle_sudo_stdin(&mut ssh_activate_child, deploy_defs) .await @@ -467,7 +501,7 @@ pub async fn deploy_profile( ssh_wait_command .arg(&ssh_addr) .stdin(std::process::Stdio::piped()); - + for ssh_opt in &deploy_data.merged_settings.ssh_opts { ssh_wait_command.arg(ssh_opt); } @@ -498,7 +532,11 @@ pub async fn deploy_profile( .spawn() .map_err(DeployProfileError::SSHWait)?; - if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) { + if deploy_data + .merged_settings + .interactive_sudo + .unwrap_or(false) + { trace!("[wait] Piping in sudo password"); handle_sudo_stdin(&mut ssh_wait_child, deploy_defs) .await @@ -522,7 +560,9 @@ pub async fn deploy_profile( info!("Success activating, attempting to confirm activation"); let c = confirm_profile(deploy_data, deploy_defs, temp_path, &ssh_addr).await; - recv_activated.await.map_err(|x| DeployProfileError::SSHActivateTimeout(x))?; + recv_activated + .await + .map_err(DeployProfileError::SSHActivateTimeout)?; c?; thread @@ -573,7 +613,7 @@ pub async fn revoke( .stdin(std::process::Stdio::piped()); for ssh_opt in &deploy_data.merged_settings.ssh_opts { - ssh_activate_command.arg(&ssh_opt); + ssh_activate_command.arg(ssh_opt); } let mut ssh_revoke_child = ssh_activate_command @@ -581,7 +621,11 @@ pub async fn revoke( .spawn() .map_err(RevokeProfileError::SSHSpawnRevoke)?; - if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) { + if deploy_data + .merged_settings + .interactive_sudo + .unwrap_or(false) + { trace!("[revoke] Piping in sudo password"); handle_sudo_stdin(&mut ssh_revoke_child, deploy_defs) .await diff --git a/src/lib.rs b/src/lib.rs index 91ab7c76..000e64db 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -168,6 +168,7 @@ pub struct CmdOverrides { pub interactive_sudo: Option, pub dry_activate: bool, pub remote_build: bool, + pub remote_bin_path: Option, } #[derive(PartialEq, Debug)] @@ -193,7 +194,7 @@ fn parse_fragment(fragment: &str) -> Result<(Option, Option), Pa let first_child = match ast.root().node().first_child() { Some(x) => x, - None => return Ok((None, None)) + None => return Ok((None, None)), }; let mut node_over = false; @@ -319,7 +320,10 @@ fn test_parse_flake() { ); } -pub fn parse_file<'a>(file: &'a str, attribute: &'a str) -> Result, ParseFlakeError> { +pub fn parse_file<'a>( + file: &'a str, + attribute: &'a str, +) -> Result, ParseFlakeError> { let (node, profile) = parse_fragment(attribute)?; Ok(DeployFlake { @@ -414,11 +418,16 @@ impl<'a> DeployData<'a> { fn get_profile_info(&'a self) -> Result { match self.profile.profile_settings.profile_path { - Some(ref profile_path) => Ok(ProfileInfo::ProfilePath { profile_path: profile_path.to_string() }), + Some(ref profile_path) => Ok(ProfileInfo::ProfilePath { + profile_path: profile_path.to_string(), + }), None => { let profile_user = self.get_profile_user()?; - Ok(ProfileInfo::ProfileUserAndName { profile_user, profile_name: self.profile_name.to_string() }) - }, + Ok(ProfileInfo::ProfileUserAndName { + profile_user, + profile_name: self.profile_name.to_string(), + }) + } } } } @@ -447,6 +456,9 @@ pub fn make_deploy_data<'a, 's>( if cmd_overrides.profile_user.is_some() { merged_settings.user = cmd_overrides.profile_user.clone(); } + if cmd_overrides.remote_bin_path.is_some() { + merged_settings.remote_bin_path = cmd_overrides.remote_bin_path.clone(); + } if let Some(ref ssh_opts) = cmd_overrides.ssh_opts { merged_settings.ssh_opts = ssh_opts.split(' ').map(|x| x.to_owned()).collect(); } diff --git a/src/push.rs b/src/push.rs index 6c777c64..7797e40a 100644 --- a/src/push.rs +++ b/src/push.rs @@ -57,7 +57,10 @@ pub struct PushProfileData<'a> { pub extra_build_args: &'a [String], } -pub async fn build_profile_locally(data: &PushProfileData<'_>, derivation_name: &str) -> Result<(), PushProfileError> { +pub async fn build_profile_locally( + data: &PushProfileData<'_>, + derivation_name: &str, +) -> Result<(), PushProfileError> { info!( "Building profile `{}` for node `{}`", data.deploy_data.profile_name, data.deploy_data.node_name @@ -150,7 +153,10 @@ pub async fn build_profile_locally(data: &PushProfileData<'_>, derivation_name: Ok(()) } -pub async fn build_profile_remotely(data: &PushProfileData<'_>, derivation_name: &str) -> Result<(), PushProfileError> { +pub async fn build_profile_remotely( + data: &PushProfileData<'_>, + derivation_name: &str, +) -> Result<(), PushProfileError> { info!( "Building profile `{}` for node `{}` on remote host", data.deploy_data.profile_name, data.deploy_data.node_name @@ -161,18 +167,29 @@ pub async fn build_profile_remotely(data: &PushProfileData<'_>, derivation_name: Some(ref x) => x, None => &data.deploy_data.node.node_settings.hostname, }; - let store_address = format!("ssh-ng://{}@{}", data.deploy_defs.ssh_user, hostname); - let ssh_opts_str = data.deploy_data.merged_settings.ssh_opts.join(" "); + let store_address = match data.deploy_data.merged_settings.remote_bin_path { + Some(ref remote_path) => format!( + "ssh-ng://{}@{}?remote-program={}", + data.deploy_defs.ssh_user, + hostname, + remote_path.join("nix-store").to_string_lossy() + ), + None => format!("ssh-ng://{}@{}", data.deploy_defs.ssh_user, hostname), + }; + let ssh_opts_str = data.deploy_data.merged_settings.ssh_opts.join(" "); // copy the derivation to remote host so it can be built there let copy_command_status = Command::new("nix") - .arg("--experimental-features").arg("nix-command") + .arg("--experimental-features") + .arg("nix-command") .arg("copy") - .arg("-s") // fetch dependencies from substitures, not localhost - .arg("--to").arg(&store_address) - .arg("--derivation").arg(derivation_name) + .arg("-s") // fetch dependencies from substitures, not localhost + .arg("--to") + .arg(&store_address) + .arg("--derivation") + .arg(derivation_name) .env("NIX_SSHOPTS", ssh_opts_str.clone()) .stdout(Stdio::null()) .status() @@ -186,10 +203,14 @@ pub async fn build_profile_remotely(data: &PushProfileData<'_>, derivation_name: let mut build_command = Command::new("nix"); build_command - .arg("--experimental-features").arg("nix-command") - .arg("build").arg(derivation_name) - .arg("--eval-store").arg("auto") - .arg("--store").arg(&store_address) + .arg("--experimental-features") + .arg("nix-command") + .arg("build") + .arg(derivation_name) + .arg("--eval-store") + .arg("auto") + .arg("--store") + .arg(&store_address) .args(data.extra_build_args) .env("NIX_SSHOPTS", ssh_opts_str.clone()); @@ -207,7 +228,6 @@ pub async fn build_profile_remotely(data: &PushProfileData<'_>, derivation_name: a => return Err(PushProfileError::BuildExit(a)), }; - Ok(()) } @@ -245,7 +265,13 @@ pub async fn build_profile(data: PushProfileData<'_>) -> Result<(), PushProfileE .next() .ok_or(PushProfileError::ShowDerivationEmpty)?; - let new_deriver = &if data.supports_flakes || data.deploy_data.merged_settings.remote_build.unwrap_or(false) { + let new_deriver = &if data.supports_flakes + || data + .deploy_data + .merged_settings + .remote_build + .unwrap_or(false) + { // Since nix 2.15.0 'nix build .drv' will build only the .drv file itself, not the // derivation outputs, '^out' is used to refer to outputs explicitly deriver.to_owned().to_string() + "^out" @@ -254,13 +280,16 @@ pub async fn build_profile(data: PushProfileData<'_>) -> Result<(), PushProfileE }; let path_info_output = Command::new("nix") - .arg("--experimental-features").arg("nix-command") + .arg("--experimental-features") + .arg("nix-command") .arg("path-info") .arg(&deriver) - .output().await + .output() + .await .map_err(PushProfileError::PathInfo)?; - let deriver = if std::str::from_utf8(&path_info_output.stdout).map(|s| s.trim()) == Ok(deriver) { + let deriver = if std::str::from_utf8(&path_info_output.stdout).map(|s| s.trim()) == Ok(deriver) + { // In this case we're on 2.15.0 or newer, because 'nix path-infonix path-info <...>.drv' // returns the same '<...>.drv' path. // If 'nix path-info <...>.drv' returns a different path, then we're on pre 2.15.0 nix and @@ -275,7 +304,12 @@ pub async fn build_profile(data: PushProfileData<'_>) -> Result<(), PushProfileE // 'error: path '...' is not valid'. deriver }; - if data.deploy_data.merged_settings.remote_build.unwrap_or(false) { + if data + .deploy_data + .merged_settings + .remote_build + .unwrap_or(false) + { if !data.supports_flakes { warn!("remote builds using non-flake nix are experimental"); } @@ -301,7 +335,12 @@ pub async fn push_profile(data: PushProfileData<'_>) -> Result<(), PushProfileEr // remote building guarantees that the resulting derivation is stored on the target system // no need to copy after building - if !data.deploy_data.merged_settings.remote_build.unwrap_or(false) { + if !data + .deploy_data + .merged_settings + .remote_build + .unwrap_or(false) + { info!( "Copying profile `{}` to node `{}`", data.deploy_data.profile_name, data.deploy_data.node_name @@ -323,9 +362,19 @@ pub async fn push_profile(data: PushProfileData<'_>) -> Result<(), PushProfileEr None => &data.deploy_data.node.node_settings.hostname, }; + let store_address = match data.deploy_data.merged_settings.remote_bin_path { + Some(ref bin_path) => format!( + "ssh://{}@{}?remote-program={}", + data.deploy_defs.ssh_user, + hostname, + bin_path.join("nix-store").to_string_lossy() + ), + None => format!("ssh://{}@{}", data.deploy_defs.ssh_user, hostname), + }; + let copy_exit_status = copy_command .arg("--to") - .arg(format!("ssh://{}@{}", data.deploy_defs.ssh_user, hostname)) + .arg(store_address) .arg(&data.deploy_data.profile.profile_settings.path) .env("NIX_SSHOPTS", ssh_opts_str) .status()