aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/activate.rs263
-rw-r--r--src/main.rs194
-rw-r--r--src/utils/deploy.rs49
-rw-r--r--src/utils/mod.rs53
-rw-r--r--src/utils/push.rs55
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(())
}