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(())