diff options
Diffstat (limited to '')
-rw-r--r-- | src/activate.rs | 263 | ||||
-rw-r--r-- | src/main.rs | 194 | ||||
-rw-r--r-- | src/utils/deploy.rs | 49 | ||||
-rw-r--r-- | src/utils/mod.rs | 53 | ||||
-rw-r--r-- | src/utils/push.rs | 55 |
5 files changed, 439 insertions, 175 deletions
diff --git a/src/activate.rs b/src/activate.rs index 3ad7e60..b7bf61f 100644 --- a/src/activate.rs +++ b/src/activate.rs @@ -18,6 +18,8 @@ use std::path::Path; use inotify::Inotify; +use thiserror::Error; + extern crate pretty_env_logger; #[macro_use] extern crate log; @@ -56,7 +58,29 @@ struct Opts { auto_rollback: bool, } -pub async fn deactivate(profile_path: &str) -> Result<(), Box<dyn std::error::Error>> { +#[derive(Error, Debug)] +pub enum DeactivateError { + #[error("Failed to execute the rollback command: {0}")] + RollbackError(std::io::Error), + #[error("The rollback resulted in a bad exit code: {0:?}")] + RollbackExitError(Option<i32>), + #[error("Failed to run command for listing generations: {0}")] + ListGenError(std::io::Error), + #[error("Command for listing generations resulted in a bad exit code: {0:?}")] + ListGenExitError(Option<i32>), + #[error("Error converting generation list output to utf8: {0}")] + DecodeListGenUtf8Error(#[from] std::string::FromUtf8Error), + #[error("Failed to run command for deleting generation: {0}")] + DeleteGenError(std::io::Error), + #[error("Command for deleting generations resulted in a bad exit code: {0:?}")] + DeleteGenExitError(Option<i32>), + #[error("Failed to run command for re-activating the last generation: {0}")] + ReactivateError(std::io::Error), + #[error("Command for re-activating the last generation resulted in a bad exit code: {0:?}")] + ReactivateExitError(Option<i32>), +} + +pub async fn deactivate(profile_path: &str) -> Result<(), DeactivateError> { error!("De-activating due to error"); let nix_env_rollback_exit_status = Command::new("nix-env") @@ -66,11 +90,13 @@ pub async fn deactivate(profile_path: &str) -> Result<(), Box<dyn std::error::Er .stdout(Stdio::null()) .stderr(Stdio::null()) .status() - .await?; + .await + .map_err(DeactivateError::RollbackError)?; - if !nix_env_rollback_exit_status.success() { - good_panic!("`nix-env --rollback` failed"); - } + match nix_env_rollback_exit_status.code() { + Some(0) => (), + a => return Err(DeactivateError::RollbackExitError(a)), + }; debug!("Listing generations"); @@ -79,11 +105,13 @@ pub async fn deactivate(profile_path: &str) -> Result<(), Box<dyn std::error::Er .arg(&profile_path) .arg("--list-generations") .output() - .await?; + .await + .map_err(DeactivateError::ListGenError)?; - if !nix_env_list_generations_out.status.success() { - good_panic!("Listing `nix-env` generations failed"); - } + match nix_env_list_generations_out.status.code() { + Some(0) => (), + a => return Err(DeactivateError::ListGenExitError(a)), + }; let generations_list = String::from_utf8(nix_env_list_generations_out.stdout)?; @@ -108,11 +136,13 @@ pub async fn deactivate(profile_path: &str) -> Result<(), Box<dyn std::error::Er .stdout(Stdio::null()) .stderr(Stdio::null()) .status() - .await?; + .await + .map_err(DeactivateError::DeleteGenError)?; - if !nix_env_delete_generation_exit_status.success() { - good_panic!("Failed to delete failed generation"); - } + match nix_env_delete_generation_exit_status.code() { + Some(0) => (), + a => return Err(DeactivateError::DeleteGenExitError(a)), + }; info!("Attempting to re-activate the last generation"); @@ -120,30 +150,59 @@ pub async fn deactivate(profile_path: &str) -> Result<(), Box<dyn std::error::Er .env("PROFILE", &profile_path) .current_dir(&profile_path) .status() - .await?; + .await + .map_err(DeactivateError::ReactivateError)?; - if !re_activate_exit_status.success() { - good_panic!("Failed to re-activate the last generation"); - } + match re_activate_exit_status.code() { + Some(0) => (), + a => return Err(DeactivateError::ReactivateExitError(a)), + }; Ok(()) } -async fn deactivate_on_err<A, B: core::fmt::Debug>(profile_path: &str, r: Result<A, B>) -> A { - match r { - Ok(x) => x, - Err(err) => { - error!("Deactivating due to error: {:?}", err); - match deactivate(profile_path).await { - Ok(_) => (), - Err(err) => { - error!("Error de-activating, uh-oh: {:?}", err); - } - }; - - std::process::exit(1); - } - } +#[derive(Error, Debug)] +pub enum ActivationConfirmationError { + #[error("Failed to create activation confirmation directory: {0}")] + CreateConfirmDirError(std::io::Error), + #[error("Failed to create activation confirmation file: {0}")] + CreateConfirmFileError(std::io::Error), + #[error("Failed to create inotify instance: {0}")] + CreateInotifyError(std::io::Error), + #[error("Failed to create inotify watcher: {0}")] + CreateInotifyWatcherError(std::io::Error), + #[error("Error forking process: {0}")] + ForkError(i32), +} + +#[derive(Error, Debug)] +pub enum DangerZoneError { + #[error("Timeout elapsed for confirmation: {0}")] + TimesUp(#[from] tokio::time::Elapsed), + #[error("inotify stream ended without activation confirmation")] + NoConfirmation, + #[error("There was some kind of error waiting for confirmation (todo figure it out)")] + SomeKindOfError(std::io::Error), +} + +async fn danger_zone( + profile_path: &str, + mut inotify: Inotify, + confirm_timeout: u16, +) -> Result<(), DangerZoneError> { + info!("Waiting for confirmation event..."); + + let mut buffer = [0; 32]; + let mut stream = inotify + .event_stream(&mut buffer) + .map_err(DangerZoneError::SomeKindOfError)?; + + timeout(Duration::from_secs(confirm_timeout as u64), stream.next()) + .await? + .ok_or(DangerZoneError::NoConfirmation)? + .map_err(DangerZoneError::SomeKindOfError)?; + + Ok(()) } pub async fn activation_confirmation( @@ -151,59 +210,67 @@ pub async fn activation_confirmation( temp_path: String, confirm_timeout: u16, closure: String, -) -> Result<(), Box<dyn std::error::Error>> { +) -> Result<(), ActivationConfirmationError> { let lock_hash = &closure["/nix/store/".len()..]; let lock_path = format!("{}/activating-{}", temp_path, lock_hash); if let Some(parent) = Path::new(&lock_path).parent() { - fs::create_dir_all(parent).await?; + fs::create_dir_all(parent) + .await + .map_err(ActivationConfirmationError::CreateConfirmDirError)?; } - fs::File::create(&lock_path).await?; + fs::File::create(&lock_path) + .await + .map_err(ActivationConfirmationError::CreateConfirmDirError)?; - let mut inotify = Inotify::init()?; - inotify.add_watch(lock_path, inotify::WatchMask::DELETE)?; + let mut inotify = + Inotify::init().map_err(ActivationConfirmationError::CreateConfirmDirError)?; + inotify + .add_watch(lock_path, inotify::WatchMask::DELETE) + .map_err(ActivationConfirmationError::CreateConfirmDirError)?; - match fork::daemon(false, false).map_err(|x| x.to_string())? { - fork::Fork::Child => { - std::thread::spawn(move || { + if let fork::Fork::Child = + fork::daemon(false, false).map_err(ActivationConfirmationError::ForkError)? + { + std::thread::spawn(move || { let mut rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async move { - info!("Waiting for confirmation event..."); - - let mut buffer = [0; 32]; - let mut stream = - deactivate_on_err(&profile_path, inotify.event_stream(&mut buffer)).await; - - deactivate_on_err( - &profile_path, - deactivate_on_err( - &profile_path, - deactivate_on_err( - &profile_path, - timeout(Duration::from_secs(confirm_timeout as u64), stream.next()) - .await, - ) - .await - .ok_or("Watcher ended prematurely"), - ) - .await, - ) - .await; + if let Err(err) = danger_zone(&profile_path, inotify, confirm_timeout).await { + if let Err(err) = deactivate(&profile_path).await { + good_panic!("Error de-activating due to another error in confirmation thread, oh no...: {}", err); + } + + good_panic!("Error in confirmation thread: {}", err); + } }); }) .join() .unwrap(); - info!("Confirmation successful!"); - - std::process::exit(0); - } - fork::Fork::Parent(_) => { - std::process::exit(0); - } + info!("Confirmation successful!"); } + + std::process::exit(0); +} + +#[derive(Error, Debug)] +pub enum ActivateError { + #[error("Failed to execute the command for setting profile: {0}")] + SetProfileError(std::io::Error), + #[error("The command for setting profile resulted in a bad exit code: {0:?}")] + SetProfileExitError(Option<i32>), + #[error("Error removing profile after bootstrap failed: {0}")] + RemoveGenerationErr(std::io::Error), + #[error("Failed to execute the activation script: {0}")] + RunActivateError(std::io::Error), + #[error("The activation script resulted in a bad exit code: {0:?}")] + RunActivateExitError(Option<i32>), + #[error("There was an error de-activating after an error was encountered: {0}")] + DeactivateError(#[from] DeactivateError), + #[error("Failed to get activation confirmation: {0}")] + ActivationConfirmationError(#[from] ActivationConfirmationError), } pub async fn activate( @@ -214,7 +281,7 @@ pub async fn activate( temp_path: String, confirm_timeout: u16, magic_rollback: bool, -) -> Result<(), Box<dyn std::error::Error>> { +) -> Result<(), ActivateError> { info!("Activating profile"); let nix_env_set_exit_status = Command::new("nix-env") @@ -224,7 +291,8 @@ pub async fn activate( .arg(&closure) .stdout(Stdio::null()) .status() - .await?; + .await + .map_err(ActivateError::SetProfileError)?; if !nix_env_set_exit_status.success() { good_panic!("Failed to update nix-env generation"); @@ -243,39 +311,48 @@ pub async fn activate( match bootstrap_status { Ok(s) if s.success() => (), _ => { - tokio::fs::remove_file(&profile_path).await?; + tokio::fs::remove_file(&profile_path) + .await + .map_err(ActivateError::RemoveGenerationErr)?; good_panic!("Failed to execute bootstrap command"); } } } - let activate_status = Command::new(format!("{}/deploy-rs-activate", profile_path)) + let activate_status = match Command::new(format!("{}/deploy-rs-activate", profile_path)) .env("PROFILE", &profile_path) .current_dir(&profile_path) .status() - .await; - - let activate_status_all = match activate_status { - Ok(s) if s.success() => Ok(()), - Ok(_) => Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Activation did not succeed", - )), - Err(x) => Err(x), + .await + .map_err(ActivateError::RunActivateError) + { + Ok(x) => x, + Err(e) => { + deactivate(&profile_path).await?; + return Err(e); + } }; - deactivate_on_err(&profile_path, activate_status_all).await; + match activate_status.code() { + Some(0) => (), + a => { + deactivate(&profile_path).await?; + return Err(ActivateError::RunActivateExitError(a)); + } + }; info!("Activation succeeded!"); if magic_rollback { - info!("Performing activation confirmation steps"); - deactivate_on_err( - &profile_path, - activation_confirmation(profile_path.clone(), temp_path, confirm_timeout, closure) - .await, - ) - .await; + match activation_confirmation(profile_path.clone(), temp_path, confirm_timeout, closure) + .await + { + Ok(()) => {} + Err(err) => { + deactivate(&profile_path).await?; + return Err(ActivateError::ActivationConfirmationError(err)); + } + }; } Ok(()) @@ -291,7 +368,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { let opts: Opts = Opts::parse(); - activate( + match activate( opts.profile_path, opts.closure, opts.bootstrap_cmd, @@ -300,7 +377,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { opts.confirm_timeout, opts.magic_rollback, ) - .await?; + .await + { + Ok(()) => (), + Err(err) => good_panic!("An error: {}", err), + } Ok(()) } diff --git a/src/main.rs b/src/main.rs index 5dc6bb9..0dd7d45 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,8 @@ use tokio::process::Command; use merge::Merge; +use thiserror::Error; + extern crate pretty_env_logger; #[macro_use] @@ -73,6 +75,16 @@ struct Opts { temp_path: Option<String>, } +#[derive(Error, Debug)] +pub enum PushAllProfilesError { + #[error("Failed to push profile `{0}`: {1}")] + PushProfileError(String, utils::push::PushProfileError), + #[error("No profile named `{0}` was found")] + ProfileNotFound(String), + #[error("Error processing deployment definitions: {0}")] + DeployDataDefsError(#[from] utils::DeployDataDefsError), +} + async fn push_all_profiles( node: &utils::data::Node, node_name: &str, @@ -83,7 +95,7 @@ async fn push_all_profiles( cmd_overrides: &utils::CmdOverrides, keep_result: bool, result_path: Option<&str>, -) -> Result<(), Box<dyn std::error::Error>> { +) -> Result<(), PushAllProfilesError> { info!("Pushing all profiles for `{}`", node_name); let mut profiles_list: Vec<&str> = node @@ -103,7 +115,11 @@ async fn push_all_profiles( for profile_name in profiles_list { let profile = match node.node_settings.profiles.get(profile_name) { Some(x) => x, - None => good_panic!("No profile was found named `{}`", profile_name), + None => { + return Err(PushAllProfilesError::ProfileNotFound( + profile_name.to_owned(), + )) + } }; let mut merged_settings = top_settings.clone(); @@ -117,9 +133,9 @@ async fn push_all_profiles( profile, profile_name, cmd_overrides, - )?; + ); - let deploy_defs = deploy_data.defs(); + let deploy_defs = deploy_data.defs()?; utils::push::push_profile( supports_flakes, @@ -130,19 +146,29 @@ async fn push_all_profiles( keep_result, result_path, ) - .await?; + .await + .map_err(|e| PushAllProfilesError::PushProfileError(profile_name.to_owned(), e))?; } Ok(()) } -#[inline] +#[derive(Error, Debug)] +pub enum DeployAllProfilesError { + #[error("Failed to deploy profile `{0}`: {1}")] + DeployProfileError(String, utils::deploy::DeployProfileError), + #[error("No profile named `{0}` was found")] + ProfileNotFound(String), + #[error("Error processing deployment definitions: {0}")] + DeployDataDefsError(#[from] utils::DeployDataDefsError), +} + async fn deploy_all_profiles( node: &utils::data::Node, node_name: &str, top_settings: &utils::data::GenericSettings, cmd_overrides: &utils::CmdOverrides, -) -> Result<(), Box<dyn std::error::Error>> { +) -> Result<(), DeployAllProfilesError> { info!("Deploying all profiles for `{}`", node_name); let mut profiles_list: Vec<&str> = node @@ -162,7 +188,11 @@ async fn deploy_all_profiles( for profile_name in profiles_list { let profile = match node.node_settings.profiles.get(profile_name) { Some(x) => x, - None => good_panic!("No profile was found named `{}`", profile_name), + None => { + return Err(DeployAllProfilesError::ProfileNotFound( + profile_name.to_owned(), + )) + } }; let mut merged_settings = top_settings.clone(); @@ -176,19 +206,20 @@ async fn deploy_all_profiles( profile, profile_name, cmd_overrides, - )?; + ); - let deploy_defs = deploy_data.defs(); + let deploy_defs = deploy_data.defs()?; - utils::deploy::deploy_profile(&deploy_data, &deploy_defs).await?; + utils::deploy::deploy_profile(&deploy_data, &deploy_defs) + .await + .map_err(|e| DeployAllProfilesError::DeployProfileError(profile_name.to_owned(), e))?; } Ok(()) } /// Returns if the available Nix installation supports flakes -#[inline] -async fn test_flake_support() -> Result<bool, Box<dyn std::error::Error>> { +async fn test_flake_support() -> Result<bool, std::io::Error> { debug!("Checking for flake support"); Ok(Command::new("nix") @@ -202,7 +233,19 @@ async fn test_flake_support() -> Result<bool, Box<dyn std::error::Error>> { .success()) } -async fn check_deployment(supports_flakes: bool, repo: &str, extra_build_args: &[String]) -> () { +#[derive(Error, Debug)] +enum CheckDeploymentError { + #[error("Failed to execute nix eval command: {0}")] + NixCheckError(#[from] std::io::Error), + #[error("Evaluation resulted in a bad exit code: {0:?}")] + NixCheckExitError(Option<i32>), +} + +async fn check_deployment( + supports_flakes: bool, + repo: &str, + extra_build_args: &[String], +) -> Result<(), CheckDeploymentError> { info!("Running checks for flake in {}", repo); let mut c = match supports_flakes { @@ -227,16 +270,28 @@ async fn check_deployment(supports_flakes: bool, repo: &str, extra_build_args: & check_command = check_command.arg(extra_arg); } - let check_status = match check_command.status().await { - Ok(x) => x, - Err(err) => good_panic!("Error running checks for the given flake repo: {:?}", err), + let check_status = check_command.status().await?; + + match check_status.code() { + Some(0) => (), + a => return Err(CheckDeploymentError::NixCheckExitError(a)), }; - if !check_status.success() { - good_panic!("Checks failed for the given flake repo"); - } + Ok(()) +} - () +#[derive(Error, Debug)] +enum GetDeploymentDataError { + #[error("Failed to execute nix eval command: {0}")] + NixEvalError(std::io::Error), + #[error("Failed to read output from evaluation: {0}")] + NixEvalOutError(std::io::Error), + #[error("Evaluation resulted in a bad exit code: {0:?}")] + NixEvalExitError(Option<i32>), + #[error("Error converting evaluation output to utf8: {0}")] + DecodeUtf8Error(#[from] std::string::FromUtf8Error), + #[error("Error decoding the JSON from evaluation: {0}")] + DecodeJsonError(#[from] serde_json::error::Error), } /// Evaluates the Nix in the given `repo` and return the processed Data from it @@ -244,7 +299,7 @@ async fn get_deployment_data( supports_flakes: bool, repo: &str, extra_build_args: &[String], -) -> Result<utils::data::Data, Box<dyn std::error::Error>> { +) -> Result<utils::data::Data, GetDeploymentDataError> { info!("Evaluating flake in {}", repo); let mut c = match supports_flakes { @@ -273,22 +328,46 @@ async fn get_deployment_data( build_command = build_command.arg(extra_arg); } - let build_child = build_command.stdout(Stdio::piped()).spawn()?; + let build_child = build_command + .stdout(Stdio::piped()) + .spawn() + .map_err(GetDeploymentDataError::NixEvalError)?; - let build_output = build_child.wait_with_output().await?; + let build_output = build_child + .wait_with_output() + .await + .map_err(GetDeploymentDataError::NixEvalOutError)?; - if !build_output.status.success() { - good_panic!( - "Error building deploy props for the provided flake: {}", - repo - ); - } + match build_output.status.code() { + Some(0) => (), + a => return Err(GetDeploymentDataError::NixEvalExitError(a)), + }; let data_json = String::from_utf8(build_output.stdout)?; Ok(serde_json::from_str(&data_json)?) } +#[derive(Error, Debug)] +enum RunDeployError { + #[error("Failed to deploy profile: {0}")] + DeployProfileError(#[from] utils::deploy::DeployProfileError), + #[error("Failed to push profile: {0}")] + PushProfileError(#[from] utils::push::PushProfileError), + #[error("Failed to deploy all profiles: {0}")] + DeployAllProfilesError(#[from] DeployAllProfilesError), + #[error("Failed to push all profiles: {0}")] + PushAllProfilesError(#[from] PushAllProfilesError), + #[error("No profile named `{0}` was found")] + ProfileNotFound(String), + #[error("No node named `{0}` was found")] + NodeNotFound(String), + #[error("Profile was provided without a node name")] + ProfileWithoutNode, + #[error("Error processing deployment definitions: {0}")] + DeployDataDefsError(#[from] utils::DeployDataDefsError), +} + async fn run_deploy( deploy_flake: utils::DeployFlake<'_>, data: utils::data::Data, @@ -297,16 +376,16 @@ async fn run_deploy( cmd_overrides: utils::CmdOverrides, keep_result: bool, result_path: Option<&str>, -) -> Result<(), Box<dyn std::error::Error>> { +) -> Result<(), RunDeployError> { match (deploy_flake.node, deploy_flake.profile) { (Some(node_name), Some(profile_name)) => { let node = match data.nodes.get(node_name) { Some(x) => x, - None => good_panic!("No node was found named `{}`", node_name), + None => return Err(RunDeployError::NodeNotFound(node_name.to_owned())), }; let profile = match node.node_settings.profiles.get(profile_name) { Some(x) => x, - None => good_panic!("No profile was found named `{}`", profile_name), + None => return Err(RunDeployError::ProfileNotFound(profile_name.to_owned())), }; let deploy_data = utils::make_deploy_data( @@ -316,9 +395,9 @@ async fn run_deploy( profile, profile_name, &cmd_overrides, - )?; + ); - let deploy_defs = deploy_data.defs(); + let deploy_defs = deploy_data.defs()?; utils::push::push_profile( supports_flakes, @@ -336,7 +415,7 @@ async fn run_deploy( (Some(node_name), None) => { let node = match data.nodes.get(node_name) { Some(x) => x, - None => good_panic!("No node was found named `{}`", node_name), + None => return Err(RunDeployError::NodeNotFound(node_name.to_owned())), }; push_all_profiles( @@ -377,16 +456,33 @@ async fn run_deploy( .await?; } } - (None, Some(_)) => { - good_panic!("Profile provided without a node, this is not (currently) supported") - } + (None, Some(_)) => return Err(RunDeployError::ProfileWithoutNode), }; Ok(()) } -#[tokio::main] -async fn main() -> Result<(), Box<dyn std::error::Error>> { +#[derive(Error, Debug)] +enum RunError { + #[error("Failed to deploy all profiles: {0}")] + DeployAllProfilesError(#[from] DeployAllProfilesError), + #[error("Failed to push all profiles: {0}")] + PushAllProfilesError(#[from] PushAllProfilesError), + #[error("Failed to deploy profile: {0}")] + DeployProfileError(#[from] utils::deploy::DeployProfileError), + #[error("Failed to push profile: {0}")] + PushProfileError(#[from] utils::push::PushProfileError), + #[error("Failed to test for flake support: {0}")] + FlakeTestError(std::io::Error), + #[error("Failed to check deployment: {0}")] + CheckDeploymentError(#[from] CheckDeploymentError), + #[error("Failed to evaluate deployment data: {0}")] + GetDeploymentDataError(#[from] GetDeploymentDataError), + #[error("Error running deploy: {0}")] + RunDeployError(#[from] RunDeployError), +} + +async fn run() -> Result<(), RunError> { if std::env::var("DEPLOY_LOG").is_err() { std::env::set_var("DEPLOY_LOG", "info"); } @@ -409,14 +505,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { confirm_timeout: opts.confirm_timeout, }; - let supports_flakes = test_flake_support().await?; + let supports_flakes = test_flake_support() + .await + .map_err(RunError::FlakeTestError)?; if !supports_flakes { warn!("A Nix version without flakes support was detected, support for this is work in progress"); } if !opts.skip_checks { - check_deployment(supports_flakes, deploy_flake.repo, &opts.extra_build_args).await; + check_deployment(supports_flakes, deploy_flake.repo, &opts.extra_build_args).await?; } let data = @@ -437,3 +535,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { Ok(()) } + +#[tokio::main] +async fn main() -> Result<(), Box<dyn std::error::Error>> { + match run().await { + Ok(()) => (), + Err(err) => good_panic!("An error: {}", err), + } + + Ok(()) +} diff --git a/src/utils/deploy.rs b/src/utils/deploy.rs index 8ed7d8c..f395a3a 100644 --- a/src/utils/deploy.rs +++ b/src/utils/deploy.rs @@ -5,6 +5,8 @@ use std::borrow::Cow; use tokio::process::Command; +use thiserror::Error; + fn build_activate_command( activate_path_str: String, sudo: &Option<String>, @@ -72,10 +74,26 @@ fn test_activation_command_builder() { ); } +#[derive(Error, Debug)] +pub enum DeployProfileError { + #[error("Failed to calculate activate bin path from deploy bin path: {0}")] + DeployPathToActivatePathError(#[from] super::DeployPathToActivatePathError), + #[error("Failed to run activation command over SSH: {0}")] + SSHActivateError(std::io::Error), + #[error("Activation over SSH resulted in a bad exit code: {0:?}")] + SSHActivateExitError(Option<i32>), + #[error("Failed to run confirmation command over SSH (the server should roll back): {0}")] + SSHConfirmError(std::io::Error), + #[error( + "Confirming activation over SSH resulted in a bad exit code (the server should roll back): {0:?}" + )] + SSHConfirmExitError(Option<i32>), +} + pub async fn deploy_profile( deploy_data: &super::DeployData<'_>, deploy_defs: &super::DeployDefs<'_>, -) -> Result<(), Box<dyn std::error::Error>> { +) -> Result<(), DeployProfileError> { info!( "Activating profile `{}` for node `{}`", deploy_data.profile_name, deploy_data.node_name @@ -122,11 +140,16 @@ pub async fn deploy_profile( ssh_command = ssh_command.arg(ssh_opt); } - let ssh_exit_status = ssh_command.arg(self_activate_command).status().await?; + let ssh_exit_status = ssh_command + .arg(self_activate_command) + .status() + .await + .map_err(DeployProfileError::SSHActivateError)?; - if !ssh_exit_status.success() { - good_panic!("Activation over SSH failed"); - } + match ssh_exit_status.code() { + Some(0) => (), + a => return Err(DeployProfileError::SSHActivateExitError(a)), + }; info!("Success activating!"); @@ -153,14 +176,16 @@ pub async fn deploy_profile( confirm_command ); - let ssh_exit_status = ssh_confirm_command.arg(confirm_command).status().await?; + let ssh_exit_status = ssh_confirm_command + .arg(confirm_command) + .status() + .await + .map_err(DeployProfileError::SSHConfirmError)?; - if !ssh_exit_status.success() { - good_panic!( - "Failed to confirm deployment, the node will roll back in <{} seconds", - confirm_timeout - ); - } + match ssh_exit_status.code() { + Some(0) => (), + a => return Err(DeployProfileError::SSHConfirmExitError(a)), + }; info!("Deployment confirmed."); } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index a0e62e1..19d0948 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -7,6 +7,8 @@ use std::path::PathBuf; use merge::Merge; +use thiserror::Error; + #[macro_export] macro_rules! good_panic { ($($tts:tt)*) => {{ @@ -112,8 +114,18 @@ pub struct DeployDefs<'a> { pub sudo: Option<String>, } +#[derive(Error, Debug)] +pub enum DeployDataDefsError { + #[error("Neither `user` nor `sshUser` are set for profile {0} of node {1}")] + NoProfileUser(String, String), + #[error("Error reading current executable path: {0}")] + ExecutablePathNotFound(std::io::Error), + #[error("Executable was not in the Nix store")] + NotNixStored, +} + impl<'a> DeployData<'a> { - pub fn defs(&'a self) -> DeployDefs<'a> { + pub fn defs(&'a self) -> Result<DeployDefs<'a>, DeployDataDefsError> { let ssh_user: Cow<str> = match self.merged_settings.ssh_user { Some(ref u) => u.into(), None => whoami::username().into(), @@ -123,11 +135,12 @@ impl<'a> DeployData<'a> { Some(ref x) => x.into(), None => match self.merged_settings.ssh_user { Some(ref x) => x.into(), - None => good_panic!( - "Neither user nor sshUser set for profile `{}` of node `{}`", - self.profile_name, - self.node_name - ), + None => { + return Err(DeployDataDefsError::NoProfileUser( + self.profile_name.to_owned(), + self.node_name.to_owned(), + )) + } }, }; @@ -149,19 +162,19 @@ impl<'a> DeployData<'a> { }; let current_exe = - std::env::current_exe().expect("Expected to find current executable path"); + std::env::current_exe().map_err(DeployDataDefsError::ExecutablePathNotFound)?; if !current_exe.starts_with("/nix/store/") { - good_panic!("The deploy binary must be in the Nix store"); + return Err(DeployDataDefsError::NotNixStored); } - DeployDefs { + Ok(DeployDefs { ssh_user, profile_user, profile_path, current_exe, sudo, - } + }) } } @@ -172,7 +185,7 @@ pub fn make_deploy_data<'a, 's>( profile: &'a data::Profile, profile_name: &'a str, cmd_overrides: &'a CmdOverrides, -) -> Result<DeployData<'a>, Box<dyn std::error::Error>> { +) -> DeployData<'a> { let mut merged_settings = top_settings.clone(); merged_settings.merge(node.generic_settings.clone()); merged_settings.merge(profile.generic_settings.clone()); @@ -196,7 +209,7 @@ pub fn make_deploy_data<'a, 's>( merged_settings.magic_rollback = Some(magic_rollback); } - Ok(DeployData { + DeployData { profile, profile_name, node, @@ -205,19 +218,27 @@ pub fn make_deploy_data<'a, 's>( cmd_overrides, merged_settings, - }) + } +} + +#[derive(Error, Debug)] +pub enum DeployPathToActivatePathError { + #[error("Deploy path did not have a parent directory")] + PathTooShort, + #[error("Deploy path was not valid utf8")] + InvalidUtf8, } pub fn deploy_path_to_activate_path_str( deploy_path: &std::path::Path, -) -> Result<String, Box<dyn std::error::Error>> { +) -> Result<String, DeployPathToActivatePathError> { Ok(format!( "{}/activate", deploy_path .parent() - .ok_or("Deploy path too short")? + .ok_or(DeployPathToActivatePathError::PathTooShort)? .to_str() - .ok_or("Deploy path is not valid utf8")? + .ok_or(DeployPathToActivatePathError::InvalidUtf8)? .to_owned() )) } diff --git a/src/utils/push.rs b/src/utils/push.rs index 5e87d5c..c79a004 100644 --- a/src/utils/push.rs +++ b/src/utils/push.rs @@ -5,6 +5,26 @@ use std::process::Stdio; use tokio::process::Command; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum PushProfileError { + #[error("Failed to calculate activate bin path from deploy bin path: {0}")] + DeployPathToActivatePathError(#[from] super::DeployPathToActivatePathError), + #[error("Failed to run Nix build command: {0}")] + BuildError(std::io::Error), + #[error("Nix build command resulted in a bad exit code: {0:?}")] + BuildExitError(Option<i32>), + #[error("Failed to run Nix sign command: {0}")] + SignError(std::io::Error), + #[error("Nix sign command resulted in a bad exit code: {0:?}")] + SignExitError(Option<i32>), + #[error("Failed to run Nix copy command: {0}")] + CopyError(std::io::Error), + #[error("Nix copy command resulted in a bad exit code: {0:?}")] + CopyExitError(Option<i32>), +} + pub async fn push_profile( supports_flakes: bool, check_sigs: bool, @@ -13,7 +33,7 @@ pub async fn push_profile( deploy_defs: &super::DeployDefs<'_>, keep_result: bool, result_path: Option<&str>, -) -> Result<(), Box<dyn std::error::Error>> { +) -> Result<(), PushProfileError> { info!( "Building profile `{}` for node `{}`", deploy_data.profile_name, deploy_data.node_name @@ -57,11 +77,16 @@ pub async fn push_profile( (false, true) => build_command.arg("--no-link"), }; - let build_exit_status = build_command.stdout(Stdio::null()).status().await?; + let build_exit_status = build_command + .stdout(Stdio::null()) + .status() + .await + .map_err(PushProfileError::BuildError)?; - if !build_exit_status.success() { - good_panic!("`nix build` failed"); - } + match build_exit_status.code() { + Some(0) => (), + a => return Err(PushProfileError::BuildExitError(a)), + }; if let Ok(local_key) = std::env::var("LOCAL_KEY") { info!( @@ -81,11 +106,13 @@ pub async fn push_profile( .stdout(Stdio::null()) .stderr(Stdio::null()) .status() - .await?; + .await + .map_err(PushProfileError::SignError)?; - if !sign_exit_status.success() { - good_panic!("`nix sign-paths` failed"); - } + match sign_exit_status.code() { + Some(0) => (), + a => return Err(PushProfileError::SignExitError(a)), + }; } debug!( @@ -127,11 +154,13 @@ pub async fn push_profile( )?) .env("NIX_SSHOPTS", ssh_opts_str) .status() - .await?; + .await + .map_err(PushProfileError::CopyError)?; - if !copy_exit_status.success() { - good_panic!("`nix copy` failed"); - } + match copy_exit_status.code() { + Some(0) => (), + a => return Err(PushProfileError::CopyExitError(a)), + }; Ok(()) } |