diff --git a/src/bin/activate.rs b/src/bin/activate.rs index 9cff9fa..ac0a4da 100644 --- a/src/bin/activate.rs +++ b/src/bin/activate.rs @@ -24,6 +24,8 @@ use thiserror::Error; use log::{debug, error, info, warn}; +use deploy::command; + /// Remote activation utility for deploy-rs #[derive(Clap, Debug)] #[clap(version = "1.0", author = "Serokell ")] @@ -121,59 +123,81 @@ struct RevokeOpts { profile_name: Option, } +#[derive(Error, Debug)] +pub enum RollbackError {} + +impl command::HasCommandError for RollbackError { + fn title() -> String { + "Nix rollback".to_string() + } +} + +#[derive(Error, Debug)] +pub enum ListGenError {} + +impl command::HasCommandError for ListGenError { + fn title() -> String { + "Nix list generations".to_string() + } +} + +#[derive(Error, Debug)] +pub enum DeleteGenError {} + +impl command::HasCommandError for DeleteGenError { + fn title() -> String { + "Nix delete generations".to_string() + } +} + +#[derive(Error, Debug)] +pub enum ReactivateError {} + +impl command::HasCommandError for ReactivateError { + fn title() -> String { + "Nix reactivate last generation".to_string() + } +} + #[derive(Error, Debug)] pub enum DeactivateError { - #[error("Failed to execute the rollback command: {0}")] - Rollback(std::io::Error), - #[error("The rollback resulted in a bad exit code: {0:?}")] - RollbackExit(Option), - #[error("Failed to run command for listing generations: {0}")] - ListGen(std::io::Error), - #[error("Command for listing generations resulted in a bad exit code: {0:?}")] - ListGenExit(Option), + #[error("{0}")] + Rollback(#[from] command::CommandError), + #[error("{0}")] + ListGen(#[from] command::CommandError), #[error("Error converting generation list output to utf8: {0}")] DecodeListGenUtf8(std::string::FromUtf8Error), - #[error("Failed to run command for deleting generation: {0}")] - DeleteGen(std::io::Error), - #[error("Command for deleting generations resulted in a bad exit code: {0:?}")] - DeleteGenExit(Option), - #[error("Failed to run command for re-activating the last generation: {0}")] - Reactivate(std::io::Error), - #[error("Command for re-activating the last generation resulted in a bad exit code: {0:?}")] - ReactivateExit(Option), + #[error("{0}")] + DeleteGen(#[from] command::CommandError), + #[error("{0}")] + Reactivate(#[from] command::CommandError), } pub async fn deactivate(profile_path: &str) -> Result<(), DeactivateError> { warn!("De-activating due to error"); - let nix_env_rollback_exit_status = Command::new("nix-env") + let mut nix_env_rollback_command = Command::new("nix-env"); + nix_env_rollback_command .arg("-p") .arg(&profile_path) - .arg("--rollback") - .status() + .arg("--rollback"); + command::Command::new(nix_env_rollback_command) + .run() .await .map_err(DeactivateError::Rollback)?; - match nix_env_rollback_exit_status.code() { - Some(0) => (), - a => return Err(DeactivateError::RollbackExit(a)), - }; - debug!("Listing generations"); - let nix_env_list_generations_out = Command::new("nix-env") + let mut nix_env_list_generations_command = Command::new("nix-env"); + nix_env_list_generations_command .arg("-p") .arg(&profile_path) - .arg("--list-generations") - .output() + .arg("--list-generations"); + let nix_env_list_generations_out = command::Command::new(nix_env_list_generations_command) + .run() .await .map_err(DeactivateError::ListGen)?; - match nix_env_list_generations_out.status.code() { - Some(0) => (), - a => return Err(DeactivateError::ListGenExit(a)), - }; - let generations_list = String::from_utf8(nix_env_list_generations_out.stdout) .map_err(DeactivateError::DecodeListGenUtf8)?; @@ -190,34 +214,28 @@ 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 mut nix_env_delete_generation_command = Command::new("nix-env"); + nix_env_delete_generation_command .arg("-p") .arg(&profile_path) .arg("--delete-generations") - .arg(last_generation_id) - .status() + .arg(last_generation_id); + command::Command::new(nix_env_delete_generation_command) + .run() .await .map_err(DeactivateError::DeleteGen)?; - match nix_env_delete_generation_exit_status.code() { - Some(0) => (), - a => return Err(DeactivateError::DeleteGenExit(a)), - }; - info!("Attempting to re-activate the last generation"); - let re_activate_exit_status = Command::new(format!("{}/deploy-rs-activate", profile_path)) + let mut re_activate_command = Command::new(format!("{}/deploy-rs-activate", profile_path)); + re_activate_command .env("PROFILE", &profile_path) - .current_dir(&profile_path) - .status() + .current_dir(&profile_path); + command::Command::new(re_activate_command) + .run() .await .map_err(DeactivateError::Reactivate)?; - match re_activate_exit_status.code() { - Some(0) => (), - a => return Err(DeactivateError::ReactivateExit(a)), - }; - Ok(()) } @@ -362,17 +380,31 @@ pub async fn wait(temp_path: PathBuf, closure: String, activation_timeout: Optio Ok(()) } +#[derive(Error, Debug)] +pub enum SetProfileError {} + +impl command::HasCommandError for SetProfileError { + fn title() -> String { + "Nix profile set".to_string() + } +} + +#[derive(Error, Debug)] +pub enum RunActivateError {} + +impl command::HasCommandError for RunActivateError { + fn title() -> String { + "Nix activation script".to_string() + } +} + #[derive(Error, Debug)] pub enum ActivateError { - #[error("Failed to execute the command for setting profile: {0}")] - SetProfile(std::io::Error), - #[error("The command for setting profile resulted in a bad exit code: {0:?}")] - SetProfileExit(Option), + #[error("{0}")] + SetProfile(#[from] command::CommandError), - #[error("Failed to execute the activation script: {0}")] - RunActivate(std::io::Error), - #[error("The activation script resulted in a bad exit code: {0:?}")] - RunActivateExit(Option), + #[error("{0}")] + RunActivate(#[from] command::CommandError), #[error("There was an error de-activating after an error was encountered: {0}")] Deactivate(#[from] DeactivateError), @@ -393,21 +425,27 @@ pub async fn activate( ) -> Result<(), ActivateError> { if !dry_activate { info!("Activating profile"); - let nix_env_set_exit_status = Command::new("nix-env") + let mut nix_env_set_command = Command::new("nix-env"); + nix_env_set_command .arg("-p") .arg(&profile_path) .arg("--set") - .arg(&closure) - .status() + .arg(&closure); + let nix_env_set_exit_output = nix_env_set_command + .output() .await - .map_err(ActivateError::SetProfile)?; - match nix_env_set_exit_status.code() { + .map_err(|err| { + ActivateError::SetProfile(command::CommandError::RunError(err)) + })?; + match nix_env_set_exit_output.status.code() { Some(0) => (), - a => { + _exit_code => { if auto_rollback && !dry_activate { deactivate(&profile_path).await?; } - return Err(ActivateError::SetProfileExit(a)); + return Err(ActivateError::SetProfile( + command::CommandError::Exit(nix_env_set_exit_output, format!("{:?}", nix_env_set_command)) + )); } }; } @@ -420,14 +458,18 @@ pub async fn activate( &profile_path }; - let activate_status = match Command::new(format!("{}/deploy-rs-activate", activation_location)) + let mut activate_command = Command::new(format!("{}/deploy-rs-activate", activation_location)); + activate_command .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() + .current_dir(activation_location); + let activate_output = match activate_command + .output() .await - .map_err(ActivateError::RunActivate) + .map_err(|err| { + ActivateError::RunActivate(command::CommandError::RunError(err)) + }) { Ok(x) => x, Err(e) => { @@ -439,13 +481,15 @@ pub async fn activate( }; if !dry_activate { - match activate_status.code() { + match activate_output.status.code() { Some(0) => (), - a => { + _exit_code => { if auto_rollback { deactivate(&profile_path).await?; } - return Err(ActivateError::RunActivateExit(a)); + return Err(ActivateError::RunActivate( + command::CommandError::Exit(activate_output, format!("{:?}", activate_command)) + )); } }; diff --git a/src/cli.rs b/src/cli.rs index f3bce4d..379e7ca 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -9,6 +9,7 @@ use std::io::{stdin, stdout, Write}; use clap::{ArgMatches, Clap, FromArgMatches}; use crate as deploy; +use crate::command; use self::deploy::{DeployFlake, ParseFlakeError}; use futures_util::stream::{StreamExt, TryStreamExt}; @@ -124,12 +125,19 @@ async fn test_flake_support() -> Result { .success()) } +#[derive(Error, Debug)] +pub enum NixCheckError {} + +impl command::HasCommandError for NixCheckError { + fn title() -> String { + "Nix checking".to_string() + } +} + #[derive(Error, Debug)] pub enum CheckDeploymentError { - #[error("Failed to execute Nix checking command: {0}")] - NixCheck(#[from] std::io::Error), - #[error("Nix checking command resulted in a bad exit code: {0:?}")] - NixCheckExit(Option), + #[error("{0}")] + NixCheck(#[from] command::CommandError), } async fn check_deployment( @@ -154,24 +162,24 @@ async fn check_deployment( check_command.args(extra_build_args); - let check_status = check_command.status().await?; - - match check_status.code() { - Some(0) => (), - a => return Err(CheckDeploymentError::NixCheckExit(a)), - }; + command::Command::new(check_command).run().await.map_err(CheckDeploymentError::NixCheck)?; Ok(()) } +#[derive(Error, Debug)] +pub enum NixEvalError {} + +impl command::HasCommandError for NixEvalError { + fn title() -> String { + "Nix eval".to_string() + } +} + #[derive(Error, Debug)] pub enum GetDeploymentDataError { - #[error("Failed to execute nix eval command: {0}")] - NixEval(std::io::Error), - #[error("Failed to read output from evaluation: {0}")] - NixEvalOut(std::io::Error), - #[error("Evaluation resulted in a bad exit code: {0:?}")] - NixEvalExit(Option), + #[error("{0}")] + NixEval(#[from] command::CommandError), #[error("Error converting evaluation output to utf8: {0}")] DecodeUtf8(#[from] std::string::FromUtf8Error), #[error("Error decoding the JSON from evaluation: {0}")] @@ -190,14 +198,14 @@ async fn get_deployment_data( info!("Evaluating flake in {}", flake.repo); - let mut c = if supports_flakes { + let mut eval_command = if supports_flakes { Command::new("nix") } else { Command::new("nix-instantiate") }; if supports_flakes { - c.arg("eval") + eval_command.arg("eval") .arg("--json") .arg(format!("{}#deploy", flake.repo)) // We use --apply instead of --expr so that we don't have to deal with builtins.getFlake @@ -205,7 +213,7 @@ async fn get_deployment_data( match (&flake.node, &flake.profile) { (Some(node), Some(profile)) => { // Ignore all nodes and all profiles but the one we're evaluating - c.arg(format!( + eval_command.arg(format!( r#" deploy: (deploy // {{ @@ -223,7 +231,7 @@ async fn get_deployment_data( } (Some(node), None) => { // Ignore all nodes but the one we're evaluating - c.arg(format!( + eval_command.arg(format!( r#" deploy: (deploy // {{ @@ -237,12 +245,12 @@ async fn get_deployment_data( } (None, None) => { // We need to evaluate all profiles of all nodes anyway, so just do it strictly - c.arg("deploy: deploy") + eval_command.arg("deploy: deploy") } (None, Some(_)) => return Err(GetDeploymentDataError::ProfileNoNode), } } else { - c + eval_command .arg("--strict") .arg("--read-write-mode") .arg("--json") @@ -251,22 +259,14 @@ async fn get_deployment_data( .arg(format!("let r = import {}/.; in if builtins.isFunction r then (r {{}}).deploy else r.deploy", flake.repo)) }; - c.args(extra_build_args); + eval_command + .args(extra_build_args) + .stdout(Stdio::null()); - let build_child = c - .stdout(Stdio::piped()) - .spawn() - .map_err(GetDeploymentDataError::NixEval)?; - - let build_output = build_child - .wait_with_output() + let build_output = command::Command::new(eval_command) + .run() .await - .map_err(GetDeploymentDataError::NixEvalOut)?; - - match build_output.status.code() { - Some(0) => (), - a => return Err(GetDeploymentDataError::NixEvalExit(a)), - }; + .map_err(GetDeploymentDataError::NixEval)?; let data_json = String::from_utf8(build_output.stdout)?; diff --git a/src/command.rs b/src/command.rs new file mode 100644 index 0000000..89ab176 --- /dev/null +++ b/src/command.rs @@ -0,0 +1,72 @@ +use std::fmt; +use std::fmt::Debug; +use thiserror::Error; +use tokio::process::Command as TokioCommand; + +pub trait HasCommandError { + fn title() -> String; +} + +#[derive(Error, Debug)] +pub enum CommandError { + RunError(std::io::Error), + Exit(std::process::Output, String), + OtherError(T), +} + +impl fmt::Display for CommandError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CommandError::RunError(err) => { + write!(f, "Failed to run {} command: {}", T::title(), err,) + } + CommandError::Exit(output, cmd) => { + let stderr = match String::from_utf8(output.stderr.clone()) { + Ok(stderr) => stderr, + Err(_err) => format!("{:?}", output.stderr), + }; + write!( + f, + "{} command resulted in a bad exit code: {:?}. The failed command is provided below:\n{}\nThe stderr output is provided below:\n{}", + T::title(), + output.status.code(), + cmd, + stderr, + ) + } + CommandError::OtherError(err) => write!(f, "{}", err), + } + } +} + +/// A wrapper over `tokio::process::Command` to provide the `run` method commonly used by `deploy`. +#[derive(Debug)] +pub struct Command { + pub command: TokioCommand, +} + +impl Command { + pub fn new(command: TokioCommand) -> Command { + Command { command } + } + + pub async fn run( + &mut self, + ) -> Result> { + let output = self + .command + .output() + .await + .map_err(CommandError::RunError)?; + match output.status.code() { + Some(0) => Ok(output), + _exit_code => Err(CommandError::Exit(output, format!("{:?}", self.command))), + } + } +} + +impl fmt::Display for Command { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.command.fmt(f) + } +} diff --git a/src/deploy.rs b/src/deploy.rs index 9f79d64..389148b 100644 --- a/src/deploy.rs +++ b/src/deploy.rs @@ -9,7 +9,7 @@ use std::path::Path; use thiserror::Error; use tokio::{io::AsyncWriteExt, process::Command}; -use crate::{DeployDataDefsError, DeployDefs, ProfileInfo}; +use crate::{command, DeployDataDefsError, DeployDefs, ProfileInfo}; struct ActivateCommandData<'a> { sudo: &'a Option, @@ -259,14 +259,21 @@ async fn handle_sudo_stdin(ssh_activate_child: &mut tokio::process::Child, deplo } } + +#[derive(Error, Debug)] +pub enum SSHConfirmError { +} + +impl command::HasCommandError for SSHConfirmError { + fn title() -> String { + "SSH confirmation command (the server should roll back)".to_string() + } +} + #[derive(Error, Debug)] pub enum ConfirmProfileError { - #[error("Failed to run confirmation command over SSH (the server should roll back): {0}")] - SSHConfirm(std::io::Error), - #[error( - "Confirming activation over SSH resulted in a bad exit code (the server should roll back): {0:?}" - )] - SSHConfirmExit(Option), + #[error("{0}")] + SSHConfirm(#[from] command::CommandError), } pub async fn confirm_profile( @@ -299,23 +306,31 @@ pub async fn confirm_profile( let mut ssh_confirm_child = ssh_confirm_command .arg(confirm_command) .spawn() - .map_err(ConfirmProfileError::SSHConfirm)?; + .map_err(|err| { + ConfirmProfileError::SSHConfirm(command::CommandError::RunError(err)) + })?; 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 - .map_err(ConfirmProfileError::SSHConfirm)?; + .map_err(|err| { + ConfirmProfileError::SSHConfirm(command::CommandError::RunError(err)) + })?; } - let ssh_confirm_exit_status = ssh_confirm_child - .wait() + let ssh_confirm_exit_output = ssh_confirm_child + .wait_with_output() .await - .map_err(ConfirmProfileError::SSHConfirm)?; + .map_err(|err| { + ConfirmProfileError::SSHConfirm(command::CommandError::RunError(err)) + })?; - match ssh_confirm_exit_status.code() { + match ssh_confirm_exit_output.status.code() { Some(0) => (), - a => return Err(ConfirmProfileError::SSHConfirmExit(a)), + _exit_code => return Err(ConfirmProfileError::SSHConfirm( + command::CommandError::Exit(ssh_confirm_exit_output, format!("{:?}", ssh_confirm_command)) + )), }; info!("Deployment confirmed."); @@ -324,22 +339,35 @@ pub async fn confirm_profile( } #[derive(Error, Debug)] -pub enum DeployProfileError { +pub enum SSHActivateError { #[error("Failed to spawn activation command over SSH: {0}")] - SSHSpawnActivate(std::io::Error), + SpawnActivateError(std::io::Error), + #[error("Failed to pipe to child stdin: {0}")] + ActivatePipeError(std::io::Error), +} - #[error("Failed to run activation command over SSH: {0}")] - SSHActivate(std::io::Error), - #[error("Activating over SSH resulted in a bad exit code: {0:?}")] - SSHActivateExit(Option), +impl command::HasCommandError for SSHActivateError { + fn title() -> String { + "SSH activation command".to_string() + } +} - #[error("Failed to run wait command over SSH: {0}")] - SSHWait(std::io::Error), - #[error("Waiting over SSH resulted in a bad exit code: {0:?}")] - SSHWaitExit(Option), +#[derive(Error, Debug)] +pub enum SSHWaitError {} - #[error("Failed to pipe to child stdin: {0}")] - SSHActivatePipe(std::io::Error), +impl command::HasCommandError for SSHWaitError { + fn title() -> String { + "SSH wait command".to_string() + } +} + +#[derive(Error, Debug)] +pub enum DeployProfileError { + #[error("{0}")] + SSHActivate(#[from] command::CommandError), + + #[error("{0}")] + SSHWait(#[from] command::CommandError), #[error("Error confirming deployment: {0}")] Confirm(#[from] ConfirmProfileError), @@ -409,23 +437,35 @@ pub async fn deploy_profile( let mut ssh_activate_child = ssh_activate_command .arg(self_activate_command) .spawn() - .map_err(DeployProfileError::SSHSpawnActivate)?; + .map_err(|err| { + DeployProfileError::SSHActivate(command::CommandError::OtherError( + SSHActivateError::SpawnActivateError(err)) + ) + })?; 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 - .map_err(DeployProfileError::SSHActivatePipe)?; + .map_err(|err| { + DeployProfileError::SSHActivate(command::CommandError::OtherError( + SSHActivateError::ActivatePipeError(err) + )) + })?; } let ssh_activate_exit_status = ssh_activate_child - .wait() + .wait_with_output() .await - .map_err(DeployProfileError::SSHActivate)?; + .map_err(|err| { + DeployProfileError::SSHActivate(command::CommandError::RunError(err)) + })?; - match ssh_activate_exit_status.code() { + match ssh_activate_exit_status.status.code() { Some(0) => (), - a => return Err(DeployProfileError::SSHActivateExit(a)), + _exit_code => return Err(DeployProfileError::SSHActivate( + command::CommandError::Exit(ssh_activate_exit_status, format!("{:?}", ssh_activate_command)) + )), }; if dry_activate { @@ -450,13 +490,21 @@ pub async fn deploy_profile( let mut ssh_activate_child = ssh_activate_command .arg(self_activate_command) .spawn() - .map_err(DeployProfileError::SSHSpawnActivate)?; + .map_err(|err| { + DeployProfileError::SSHActivate(command::CommandError::OtherError( + SSHActivateError::SpawnActivateError(err) + )) + })?; 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 - .map_err(DeployProfileError::SSHActivatePipe)?; + .map_err(|err| { + DeployProfileError::SSHActivate(command::CommandError::OtherError( + SSHActivateError::ActivatePipeError(err) + )) + })?; } info!("Creating activation waiter"); @@ -477,10 +525,12 @@ pub async fn deploy_profile( let o = ssh_activate_child.wait_with_output().await; let maybe_err = match o { - Err(x) => Some(DeployProfileError::SSHActivate(x)), + Err(x) => Some(DeployProfileError::SSHActivate(command::CommandError::RunError(x))), Ok(ref x) => match x.status.code() { Some(0) => None, - a => Some(DeployProfileError::SSHActivateExit(a)), + _exit_code => Some(DeployProfileError::SSHActivate( + command::CommandError::Exit(x.clone(), format!("{:?}", ssh_activate_command)) + )), }, }; @@ -494,21 +544,30 @@ pub async fn deploy_profile( let mut ssh_wait_child = ssh_wait_command .arg(self_wait_command) .spawn() - .map_err(DeployProfileError::SSHWait)?; + .map_err(|err| { + DeployProfileError::SSHWait(command::CommandError::RunError(err)) + })?; 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 - .map_err(DeployProfileError::SSHActivatePipe)?; + .map_err(|err| { + DeployProfileError::SSHActivate(command::CommandError::OtherError( + SSHActivateError::ActivatePipeError(err) + )) + })?; } tokio::select! { - x = ssh_wait_child.wait() => { + x = ssh_wait_child.wait_with_output() => { debug!("Wait command ended"); - match x.map_err(DeployProfileError::SSHWait)?.code() { + let output = x.map_err(|err| DeployProfileError::SSHWait(command::CommandError::RunError(err)))?; + match output.status.code() { Some(0) => (), - a => return Err(DeployProfileError::SSHWaitExit(a)), + _exit_code => return Err(DeployProfileError::SSHWait( + command::CommandError::Exit(output, format!("{:?}", ssh_wait_command)) + )), }; }, x = recv_activate => { @@ -525,21 +584,28 @@ pub async fn deploy_profile( thread .await - .map_err(|x| DeployProfileError::SSHActivate(x.into()))?; + .map_err(|x| DeployProfileError::SSHActivate(command::CommandError::RunError(x.into())))?; } Ok(()) } #[derive(Error, Debug)] -pub enum RevokeProfileError { +pub enum SSHRevokeError { #[error("Failed to spawn revocation command over SSH: {0}")] - SSHSpawnRevoke(std::io::Error), + SpawnRevokeError(std::io::Error) +} - #[error("Error revoking deployment: {0}")] - SSHRevoke(std::io::Error), - #[error("Revoking over SSH resulted in a bad exit code: {0:?}")] - SSHRevokeExit(Option), +impl command::HasCommandError for SSHRevokeError { + fn title() -> String { + "SSH revoke command".to_string() + } +} + +#[derive(Error, Debug)] +pub enum RevokeProfileError { + #[error("{0}")] + SSHRevoke(#[from] command::CommandError), #[error("Deployment data invalid: {0}")] InvalidDeployDataDefs(#[from] DeployDataDefsError), @@ -577,22 +643,30 @@ pub async fn revoke( let mut ssh_revoke_child = ssh_activate_command .arg(self_revoke_command) .spawn() - .map_err(RevokeProfileError::SSHSpawnRevoke)?; + .map_err(|err| { + RevokeProfileError::SSHRevoke(command::CommandError::OtherError( + SSHRevokeError::SpawnRevokeError(err) + )) + })?; 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 - .map_err(RevokeProfileError::SSHRevoke)?; + .map_err(|err| { + RevokeProfileError::SSHRevoke(command::CommandError::RunError(err)) + })?; } let result = ssh_revoke_child.wait_with_output().await; match result { - Err(x) => Err(RevokeProfileError::SSHRevoke(x)), + Err(x) => Err(RevokeProfileError::SSHRevoke(command::CommandError::RunError(x))), Ok(ref x) => match x.status.code() { Some(0) => Ok(()), - a => Err(RevokeProfileError::SSHRevokeExit(a)), + _exit_code => Err(RevokeProfileError::SSHRevoke( + command::CommandError::Exit(x.clone(), format!("{:?}", ssh_activate_command)) + )), }, } } diff --git a/src/lib.rs b/src/lib.rs index 61fac6a..6e95336 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -148,6 +148,7 @@ pub fn init_logger( } pub mod cli; +pub mod command; pub mod data; pub mod deploy; pub mod push; diff --git a/src/push.rs b/src/push.rs index 864c336..254b8df 100644 --- a/src/push.rs +++ b/src/push.rs @@ -9,22 +9,66 @@ use std::process::Stdio; use thiserror::Error; use tokio::process::Command; +use crate::command; + #[derive(Error, Debug)] -pub enum PushProfileError { - #[error("Failed to run Nix show-derivation command: {0}")] - ShowDerivation(std::io::Error), - #[error("Nix show-derivation command resulted in a bad exit code: {0:?}")] - ShowDerivationExit(Option), +pub enum ShowDerivationError { #[error("Nix show-derivation command output contained an invalid UTF-8 sequence: {0}")] - ShowDerivationUtf8(std::str::Utf8Error), + Utf8(std::str::Utf8Error), #[error("Failed to parse the output of nix show-derivation: {0}")] - ShowDerivationParse(serde_json::Error), + Parse(serde_json::Error), #[error("Nix show-derivation output is empty")] - ShowDerivationEmpty, - #[error("Failed to run Nix build command: {0}")] - Build(std::io::Error), - #[error("Nix build command resulted in a bad exit code: {0:?}")] - BuildExit(Option), + Empty, +} + +impl command::HasCommandError for ShowDerivationError { + fn title() -> String { + "Nix show derivation".to_string() + } +} + +#[derive(Error, Debug)] +pub enum BuildError {} + +impl command::HasCommandError for BuildError { + fn title() -> String { + "Nix build".to_string() + } +} + +#[derive(Error, Debug)] +pub enum SignError {} + +impl command::HasCommandError for SignError { + fn title() -> String { + "Nix sign".to_string() + } +} + +#[derive(Error, Debug)] +pub enum CopyError {} + +impl command::HasCommandError for CopyError { + fn title() -> String { + "Nix copy".to_string() + } +} + +#[derive(Error, Debug)] +pub enum PathInfoError {} + +impl command::HasCommandError for PathInfoError { + fn title() -> String { + "Nix path-info".to_string() + } +} + +#[derive(Error, Debug)] +pub enum PushProfileError { + #[error("{0}")] + ShowDerivation(#[from] command::CommandError), + #[error("{0}")] + Build(#[from] command::CommandError), #[error( "Activation script deploy-rs-activate does not exist in profile.\n\ Did you forget to use deploy-rs#lib.<...>.activate.<...> on your profile path?" @@ -33,19 +77,14 @@ pub enum PushProfileError { #[error("Activation script activate-rs does not exist in profile.\n\ Is there a mismatch in deploy-rs used in the flake you're deploying and deploy-rs command you're running?")] ActivateRsDoesntExist, - #[error("Failed to run Nix sign command: {0}")] - Sign(std::io::Error), - #[error("Nix sign command resulted in a bad exit code: {0:?}")] - SignExit(Option), - #[error("Failed to run Nix copy command: {0}")] - Copy(std::io::Error), - #[error("Nix copy command resulted in a bad exit code: {0:?}")] - CopyExit(Option), + #[error("{0}")] + Sign(#[from] command::CommandError), + #[error("{0}")] + Copy(#[from] command::CommandError), #[error("The remote building option is not supported when using legacy nix")] RemoteBuildWithLegacyNix, - - #[error("Failed to run Nix path-info command: {0}")] - PathInfo(std::io::Error), + #[error("{0}")] + PathInfo(#[from] command::CommandError), } pub struct PushProfileData<'a> { @@ -90,20 +129,16 @@ pub async fn build_profile_locally(data: &PushProfileData<'_>, derivation_name: (false, true) => build_command.arg("--no-link"), }; - build_command.args(data.extra_build_args); - - let build_exit_status = build_command + build_command + .args(data.extra_build_args) // Logging should be in stderr, this just stops the store path from printing for no reason - .stdout(Stdio::null()) - .status() + .stdout(Stdio::null()); + + command::Command::new(build_command) + .run() .await .map_err(PushProfileError::Build)?; - match build_exit_status.code() { - Some(0) => (), - a => return Err(PushProfileError::BuildExit(a)), - }; - if !Path::new( format!( "{}/deploy-rs-activate", @@ -134,20 +169,17 @@ pub async fn build_profile_locally(data: &PushProfileData<'_>, derivation_name: data.deploy_data.profile_name, data.deploy_data.node_name ); - let sign_exit_status = Command::new("nix") + let mut sign_command = Command::new("nix"); + sign_command .arg("sign-paths") .arg("-r") .arg("-k") .arg(local_key) - .arg(&data.deploy_data.profile.profile_settings.path) - .status() + .arg(&data.deploy_data.profile.profile_settings.path); + command::Command::new(sign_command) + .run() .await .map_err(PushProfileError::Sign)?; - - match sign_exit_status.code() { - Some(0) => (), - a => return Err(PushProfileError::SignExit(a)), - }; } Ok(()) } @@ -169,44 +201,36 @@ pub async fn build_profile_remotely(data: &PushProfileData<'_>, derivation_name: // copy the derivation to remote host so it can be built there - let copy_command_status = Command::new("nix").arg("copy") + let mut copy_command = Command::new("nix"); + copy_command + .arg("copy") .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() + .stdout(Stdio::null()); + command::Command::new(copy_command) + .run() .await .map_err(PushProfileError::Copy)?; - match copy_command_status.code() { - Some(0) => (), - a => return Err(PushProfileError::CopyExit(a)), - }; - let mut build_command = Command::new("nix"); build_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()); + .env("NIX_SSHOPTS", ssh_opts_str.clone()) + // Logging should be in stderr, this just stops the store path from printing for no reason + .stdout(Stdio::null()); debug!("build command: {:?}", build_command); - let build_exit_status = build_command - // Logging should be in stderr, this just stops the store path from printing for no reason - .stdout(Stdio::null()) - .status() + command::Command::new(build_command) + .run() .await .map_err(PushProfileError::Build)?; - match build_exit_status.code() { - Some(0) => (), - a => return Err(PushProfileError::BuildExit(a)), - }; - - Ok(()) } @@ -223,26 +247,32 @@ pub async fn build_profile(data: PushProfileData<'_>) -> Result<(), PushProfileE .arg("show-derivation") .arg(&data.deploy_data.profile.profile_settings.path); - let show_derivation_output = show_derivation_command - .output() + let show_derivation_output = command::Command::new(show_derivation_command) + .run() .await .map_err(PushProfileError::ShowDerivation)?; - match show_derivation_output.status.code() { - Some(0) => (), - a => return Err(PushProfileError::ShowDerivationExit(a)), - }; - let derivation_info: HashMap<&str, serde_json::value::Value> = serde_json::from_str( - std::str::from_utf8(&show_derivation_output.stdout) - .map_err(PushProfileError::ShowDerivationUtf8)?, + std::str::from_utf8(&show_derivation_output.stdout).map_err(|err| { + PushProfileError::ShowDerivation(command::CommandError::OtherError( + ShowDerivationError::Utf8(err) + )) + })? ) - .map_err(PushProfileError::ShowDerivationParse)?; + .map_err(|err| { + PushProfileError::ShowDerivation(command::CommandError::OtherError( + ShowDerivationError::Parse(err) + )) + })?; let &deriver = derivation_info .keys() .next() - .ok_or(PushProfileError::ShowDerivationEmpty)?; + .ok_or( + PushProfileError::ShowDerivation(command::CommandError::OtherError( + ShowDerivationError::Empty + )) + )?; let new_deriver = &if data.supports_flakes { // Since nix 2.15.0 'nix build .drv' will build only the .drv file itself, not the @@ -252,11 +282,14 @@ pub async fn build_profile(data: PushProfileData<'_>) -> Result<(), PushProfileE deriver.to_owned() }; - let path_info_output = Command::new("nix") + let mut path_info_command = Command::new("nix"); + path_info_command .arg("--experimental-features").arg("nix-command") .arg("path-info") - .arg(&deriver) - .output().await + .arg(&deriver); + let path_info_output = command::Command::new(path_info_command) + .run() + .await .map_err(PushProfileError::PathInfo)?; let deriver = if std::str::from_utf8(&path_info_output.stdout).map(|s| s.trim()) == Ok(deriver) { @@ -322,19 +355,15 @@ pub async fn push_profile(data: PushProfileData<'_>) -> Result<(), PushProfileEr None => &data.deploy_data.node.node_settings.hostname, }; - let copy_exit_status = copy_command + copy_command .arg("--to") .arg(format!("ssh://{}@{}", data.deploy_defs.ssh_user, hostname)) .arg(&data.deploy_data.profile.profile_settings.path) - .env("NIX_SSHOPTS", ssh_opts_str) - .status() + .env("NIX_SSHOPTS", ssh_opts_str); + command::Command::new(copy_command) + .run() .await .map_err(PushProfileError::Copy)?; - - match copy_exit_status.code() { - Some(0) => (), - a => return Err(PushProfileError::CopyExit(a)), - }; } Ok(())