From 5e20ce47ed753f5cff8ff8e2cf0e00431f1b8a3b Mon Sep 17 00:00:00 2001 From: Elaina <17bestradiol@proton.me> Date: Sun, 25 Feb 2024 13:00:53 -0300 Subject: [PATCH] Handling Errors in a proper trait, and passing it up the stack --- .../repositories/deployments/delete.rs | 8 +- .../repositories/deployments/find_by_host.rs | 8 +- .../repositories/deployments/find_by_id.rs | 8 +- .../repositories/deployments/find_by_name.rs | 8 +- .../repositories/deployments/retrieve_all.rs | 4 +- src/business/repositories/deployments/save.rs | 4 +- src/business/services/deployments/delete.rs | 18 +++- src/business/services/deployments/get.rs | 6 +- src/business/services/deployments/get_logs.rs | 10 +- src/business/services/deployments/list.rs | 11 ++- src/business/services/deployments/new.rs | 34 ++++--- src/controllers/deployments/get.rs | 50 +++++----- src/modules/cloudflare/add_dns_record.rs | 96 +++++++++++-------- src/modules/cloudflare/remove_dns_record.rs | 82 +++++++++------- .../discord/send_deployment_message.rs | 65 ++++++++----- src/modules/docker/build_image.rs | 18 ++-- src/modules/docker/create_container.rs | 4 +- src/modules/docker/delete_container.rs | 6 +- src/modules/docker/delete_image.rs | 4 +- src/modules/docker/get_internal_port.rs | 4 +- src/modules/docker/is_container_running.rs | 14 +-- src/modules/docker/restart_container.rs | 8 +- src/modules/docker/start_container.rs | 4 +- src/modules/docker/stop_container.rs | 4 +- src/types/to_json_str.rs | 23 +++-- src/utils/get_free_port.rs | 33 ++++--- src/utils/http_client/deserializable.rs | 1 + src/utils/http_client/ensure_success.rs | 68 +++++++------ src/utils/http_client/http_error.rs | 55 +++++++++++ src/utils/http_client/mod.rs | 1 + src/utils/runtime_helpers.rs | 4 +- 31 files changed, 406 insertions(+), 257 deletions(-) create mode 100644 src/utils/http_client/http_error.rs diff --git a/src/business/repositories/deployments/delete.rs b/src/business/repositories/deployments/delete.rs index f9a798a..7a99b22 100644 --- a/src/business/repositories/deployments/delete.rs +++ b/src/business/repositories/deployments/delete.rs @@ -35,22 +35,22 @@ pub async fn delete(name: String) -> Result<(), VoyagerError> { } impl VoyagerError { - pub fn delete_mongo(e: Error, name: &str) -> Self { + fn delete_mongo(e: Error, name: &str) -> Self { let message = format!("Failed to delete deployment named '{name}'! Error:{e}"); event!(Level::ERROR, message); - VoyagerError { + Self { message, status_code: StatusCode::INTERNAL_SERVER_ERROR, source: Some(e), } } - pub fn delete(name: &str) -> Self { + fn delete(name: &str) -> Self { let message = format!("Failed to find deployment named '{name}'!"); event!(Level::ERROR, message); - VoyagerError { + Self { message, status_code: StatusCode::NOT_FOUND, source: None, diff --git a/src/business/repositories/deployments/find_by_host.rs b/src/business/repositories/deployments/find_by_host.rs index 89164a0..6e2a355 100644 --- a/src/business/repositories/deployments/find_by_host.rs +++ b/src/business/repositories/deployments/find_by_host.rs @@ -26,22 +26,22 @@ pub async fn find_by_host(host: String) -> Result { } impl VoyagerError { - pub fn find_mongo_host(e: Error, host: &str) -> Self { + fn find_mongo_host(e: Error, host: &str) -> Self { let message = format!("Failed to find deployment by host '{host}'! Error:{e}"); event!(Level::ERROR, message); - VoyagerError { + Self { message, status_code: StatusCode::INTERNAL_SERVER_ERROR, source: Some(e), } } - pub fn find_null_host(host: &str) -> Self { + fn find_null_host(host: &str) -> Self { let message = format!("Failed to find deployment by host '{host}'!"); event!(Level::ERROR, message); - VoyagerError { + Self { message, status_code: StatusCode::NOT_FOUND, source: None, diff --git a/src/business/repositories/deployments/find_by_id.rs b/src/business/repositories/deployments/find_by_id.rs index fc2aca7..a0abb72 100644 --- a/src/business/repositories/deployments/find_by_id.rs +++ b/src/business/repositories/deployments/find_by_id.rs @@ -24,22 +24,22 @@ pub async fn find_by_id(id: String) -> Result { } impl VoyagerError { - pub fn find_mongo_id(e: Error, id: &str) -> Self { + fn find_mongo_id(e: Error, id: &str) -> Self { let message = format!("Failed to find deployment by id '{id}'! Error:{e}"); event!(Level::ERROR, message); - VoyagerError { + Self { message, status_code: StatusCode::INTERNAL_SERVER_ERROR, source: Some(e), } } - pub fn find_null_id(id: &str) -> Self { + fn find_null_id(id: &str) -> Self { let message = format!("Failed to find deployment by id '{id}'!"); event!(Level::ERROR, message); - VoyagerError { + Self { message, status_code: StatusCode::NOT_FOUND, source: None, diff --git a/src/business/repositories/deployments/find_by_name.rs b/src/business/repositories/deployments/find_by_name.rs index 2c377f6..d0b9803 100644 --- a/src/business/repositories/deployments/find_by_name.rs +++ b/src/business/repositories/deployments/find_by_name.rs @@ -26,22 +26,22 @@ pub async fn find_by_name(name: String) -> Result { } impl VoyagerError { - pub fn find_mongo_name(e: Error, name: &str) -> Self { + fn find_mongo_name(e: Error, name: &str) -> Self { let message = format!("Failed to find deployment named '{name}'! Error:{e}"); event!(Level::ERROR, message); - VoyagerError { + Self { message, status_code: StatusCode::INTERNAL_SERVER_ERROR, source: Some(e), } } - pub fn find_null_name(name: &str) -> Self { + fn find_null_name(name: &str) -> Self { let message = format!("Failed to find deployment named '{name}'!"); event!(Level::ERROR, message); - VoyagerError { + Self { message, status_code: StatusCode::NOT_FOUND, source: None, diff --git a/src/business/repositories/deployments/retrieve_all.rs b/src/business/repositories/deployments/retrieve_all.rs index ccaba19..5750c5e 100644 --- a/src/business/repositories/deployments/retrieve_all.rs +++ b/src/business/repositories/deployments/retrieve_all.rs @@ -59,10 +59,10 @@ pub async fn retrieve_all( } impl VoyagerError { - pub fn retrieve_all(e: Error) -> Self { + fn retrieve_all(e: Error) -> Self { let message = format!("Failed to retrieve deployments! Error: {e}"); event!(Level::ERROR, message); - VoyagerError { + Self { message, status_code: StatusCode::INTERNAL_SERVER_ERROR, source: Some(e), diff --git a/src/business/repositories/deployments/save.rs b/src/business/repositories/deployments/save.rs index 3ddd93f..28099fb 100644 --- a/src/business/repositories/deployments/save.rs +++ b/src/business/repositories/deployments/save.rs @@ -28,10 +28,10 @@ pub async fn save(deployment: Deployment) -> Result { } impl VoyagerError { - pub fn save(e: Error) -> Self { + fn save(e: Error) -> Self { let message = format!("Failed to save deployment! Error: {e}"); event!(Level::ERROR, message); - VoyagerError { + Self { message, status_code: StatusCode::INTERNAL_SERVER_ERROR, source: Some(e), diff --git a/src/business/services/deployments/delete.rs b/src/business/services/deployments/delete.rs index c13d15a..41879aa 100644 --- a/src/business/services/deployments/delete.rs +++ b/src/business/services/deployments/delete.rs @@ -1,3 +1,4 @@ +use axum::http::StatusCode; use tracing::{event, Level}; use crate::{ @@ -24,8 +25,7 @@ async fn delete(deployment: Deployment) -> Result<(), VoyagerError> { let name = deployment.container_name; if is_container_running(name.clone()).await? { - event!(Level::ERROR, "Tried to delete container that is running"); - return None; + return Err(VoyagerError::delete_running()); } delete_image(name.clone()).await?; @@ -37,7 +37,7 @@ async fn delete(deployment: Deployment) -> Result<(), VoyagerError> { // TODO: notify user via email - Some(()) + Ok(()) }; SERVICES_RUNTIME @@ -45,3 +45,15 @@ async fn delete(deployment: Deployment) -> Result<(), VoyagerError> { .await .and_then(|f| f) } + +impl VoyagerError { + fn delete_running() -> Self { + let message = "Tried to delete container that is running"; + event!(Level::ERROR, message); + Self { + message: message.to_string(), + status_code: StatusCode::BAD_REQUEST, + source: None, + } + } +} diff --git a/src/business/services/deployments/get.rs b/src/business/services/deployments/get.rs index 68dff86..6e5cdb4 100644 --- a/src/business/services/deployments/get.rs +++ b/src/business/services/deployments/get.rs @@ -1,7 +1,7 @@ -use crate::types::model::deployment::Deployment; use crate::business::repositories; +use crate::types::model::deployment::Deployment; +use crate::types::other::voyager_error::VoyagerError; - -pub async fn get(id: String) -> Option { +pub async fn get(id: String) -> Result { repositories::deployments::find_by_id(id).await } diff --git a/src/business/services/deployments/get_logs.rs b/src/business/services/deployments/get_logs.rs index 8deab37..0774fb6 100644 --- a/src/business/services/deployments/get_logs.rs +++ b/src/business/services/deployments/get_logs.rs @@ -1,11 +1,9 @@ use crate::business::repositories; use crate::modules::docker; +use crate::types::other::voyager_error::VoyagerError; +pub async fn get_logs(id: String) -> Result, VoyagerError> { + let deployment = repositories::deployments::find_by_id(id).await?; -pub async fn get_logs(id: String) -> Option> { - if let Some(deployment) = repositories::deployments::find_by_id(id).await { - return docker::get_logs(&deployment.container_name).await - } - - None + docker::get_logs(&deployment.container_name).await } diff --git a/src/business/services/deployments/list.rs b/src/business/services/deployments/list.rs index 1d5531f..757d8da 100644 --- a/src/business/services/deployments/list.rs +++ b/src/business/services/deployments/list.rs @@ -1,6 +1,11 @@ -use crate::{business::repositories, types::model::deployment::Deployment}; +use crate::{ + business::repositories, + types::{model::deployment::Deployment, other::voyager_error::VoyagerError}, +}; - -pub async fn list(repo_url: Option, branch: Option) -> Option> { +pub async fn list( + repo_url: Option, + branch: Option, +) -> Result, VoyagerError> { repositories::deployments::retrieve_all(repo_url, branch).await } diff --git a/src/business/services/deployments/new.rs b/src/business/services/deployments/new.rs index 4aa5352..07f2cb0 100644 --- a/src/business/services/deployments/new.rs +++ b/src/business/services/deployments/new.rs @@ -6,8 +6,10 @@ use crate::configs::environment::HOST_IP; use crate::modules::discord::send_deployment_message; use crate::modules::{cloudflare, traefik}; use crate::types::model::deployment; +use crate::types::other::voyager_error::VoyagerError; use crate::utils::get_free_port; use crate::{business::repositories::deployments, modules::docker}; +use axum::http::StatusCode; use mongodb::bson::Bson; use tracing::{event, Level}; use uuid::Uuid; @@ -27,23 +29,13 @@ pub async fn new( directory: &str, repo_url: &str, branch: &str, -) -> Option>> { +) -> Result { event!(Level::INFO, "Creating deployment with host {host}, Dockerfile {:?}, mode {mode}, directory {directory}, repo_url {repo_url}, branch {branch}", dockerfile); - let cloudflare_result = cloudflare::add_dns_record(host, &HOST_IP, mode).await?; + let dns_record_id = cloudflare::add_dns_record(host, &HOST_IP, mode).await?; - let dns_record_id = match cloudflare_result { - Ok(id) => id, - Err(errs) => return Some(Err(errs)), - }; - - let dockerfile_contents = match fs::read_to_string(dockerfile) { - Ok(contents) => contents, - Err(err) => { - event!(Level::ERROR, "Failed to read Dockerfile contents: {}", err); - return None; - } - }; + let dockerfile_contents = + fs::read_to_string(dockerfile).map_err(|e| VoyagerError::dockerfile_read(Box::new(e)))?; let internal_port = docker::find_internal_port(dockerfile_contents.as_str())?; let free_port = get_free_port()?; @@ -74,5 +66,17 @@ pub async fn new( // TODO: notify user via email - Some(Ok(db_id)) + Ok(db_id) +} + +impl VoyagerError { + fn dockerfile_read(e: Error) -> Self { + let message = format!("Failed to read Dockerfile contents: {e}"); + event!(Level::ERROR, message); + Self { + message, + status_code: StatusCode::INTERNAL_SERVER_ERROR, + source: Some(e), + } + } } diff --git a/src/controllers/deployments/get.rs b/src/controllers/deployments/get.rs index 04800f4..86eff4c 100644 --- a/src/controllers/deployments/get.rs +++ b/src/controllers/deployments/get.rs @@ -13,32 +13,32 @@ use crate::{ use crate::types::to_json_str; pub async fn get(Query(queries): Query>) -> impl IntoResponse { - let id_opt = queries.get("deploymentId").cloned(); + // let id_opt = queries.get("deploymentId").cloned(); - let inner = || async { - let Some(id) = id_opt else { - return Some(( - StatusCode::BAD_REQUEST, - to_json_str(&GetDeployment { deployment: None })?, - )); - }; + // let inner = || async { + // let Some(id) = id_opt else { + // return Some(( + // StatusCode::BAD_REQUEST, + // to_json_str(&GetDeployment { deployment: None })?, + // )); + // }; - match deployments::get(id).await { - Some(deployment) => Some(( - StatusCode::OK, - to_json_str(&GetDeployment { - deployment: Some(deployment), - })?, - )), - None => Some(( - StatusCode::NOT_FOUND, - to_json_str(&GetDeployment { deployment: None })?, - )), - } - }; + // match deployments::get(id).await { + // Some(deployment) => Some(( + // StatusCode::OK, + // to_json_str(&GetDeployment { + // deployment: Some(deployment), + // })?, + // )), + // None => Some(( + // StatusCode::NOT_FOUND, + // to_json_str(&GetDeployment { deployment: None })?, + // )), + // } + // }; - inner().await.unwrap_or(( - StatusCode::INTERNAL_SERVER_ERROR, - "Internal Server Error".to_string(), - )) + // inner().await.unwrap_or(( + // StatusCode::INTERNAL_SERVER_ERROR, + // "Internal Server Error".to_string(), + // )) } diff --git a/src/modules/cloudflare/add_dns_record.rs b/src/modules/cloudflare/add_dns_record.rs index 844b575..1cadb50 100644 --- a/src/modules/cloudflare/add_dns_record.rs +++ b/src/modules/cloudflare/add_dns_record.rs @@ -1,3 +1,4 @@ +use axum::http::StatusCode; use serde_json::Value; use tracing::{event, Level}; @@ -7,16 +8,13 @@ use crate::modules::cloudflare::types::cloudflare_responses::CloudflareError; use crate::modules::cloudflare::types::dns_record::DnsRecord; use crate::modules::cloudflare::CLOUDFLARE_CLIENT; use crate::types::model::deployment::Mode; +use crate::types::other::voyager_error::VoyagerError; use crate::utils::http_client::ensure_success::EnsureSuccess; use crate::utils::Error; -pub async fn add_dns_record( - host: &str, - ip: &str, - mode: &Mode, -) -> Option>> { +pub async fn add_dns_record(host: &str, ip: &str, mode: &Mode) -> Result { if *DEVELOPMENT { - return Some(Ok("devDnsRecord".to_string())); + return Ok("devDnsRecord".to_string()); } event!( @@ -37,25 +35,17 @@ pub async fn add_dns_record( }; let route = format!("zones/{}/dns_records", *CLOUDFLARE_ZONE); - let (is_success, response, status) = CLOUDFLARE_CLIENT + + let (response, status_code) = CLOUDFLARE_CLIENT .write() .await .post::(route.as_str(), Some(&dns_record)) .await - .ensure_success(false); - if !is_success { - event!( - Level::ERROR, - "Failed to send request to Add DNS Record with Cloudflare." - ); - return None; - } - + .ensure_success(false) + .map_err(|e| VoyagerError::cloudflare_add_req(Box::new(e)))?; // These are already checked by the .ensure_success(false) + is_success checks above #[allow(clippy::unwrap_used)] let response = response.unwrap().data().unwrap(); - #[allow(clippy::unwrap_used)] - let status = status.unwrap(); event!(Level::DEBUG, "Request sent to Cloudflare"); @@ -67,27 +57,55 @@ pub async fn add_dns_record( "Cloudflare request was successful with id: {}", id ); - Some(Ok(id)) + Ok(id) } else { - let failure = serde_json::from_value::(response); - let failure = match failure { - Ok(failure) => failure, - Err(err) => { - event!( - Level::ERROR, - "Failed to deserialize failed response for Cloudflare. Status was: {}. Error: {}", - status, - err - ); - return None; - } - }; - event!( - Level::DEBUG, - "Request failed with status {} and errors: {:?}", - status, - failure.errors - ); - Some(Err(failure.errors.into_iter().map(Error::from).collect())) + let failure = serde_json::from_value::(response) + .map_err(|e| VoyagerError::cloudflare_add_deserialize(Box::new(e), status_code))?; + + Err(VoyagerError::cloudflare_add_failure(&failure, status_code)) + } +} + +impl VoyagerError { + fn cloudflare_add_req(e: Error) -> Self { + let message = format!("Failed to send Add DNS request to Cloudflare. Error: {e}"); + event!(Level::ERROR, message); + Self { + message, + status_code: StatusCode::INTERNAL_SERVER_ERROR, + source: Some(e), + } + } + + fn cloudflare_add_deserialize( + e: Error, + status_code: reqwest::StatusCode) + -> Self { + + let message = + format!("Failed to deserialize Add DNS request response from Cloudflare. Status was: {status_code}. Error: {e}"); + event!(Level::ERROR, message); + Self { + message, + status_code: StatusCode::INTERNAL_SERVER_ERROR, + source: Some(e), + } + } + + fn cloudflare_add_failure(failure: &Failure, status_code: reqwest::StatusCode) -> Self { + let err = failure + .errors + .iter() + .fold(String::from("Cloudflare Errors:"), |acc, e| { + format!("{acc}\n{e}") + }); + let message = format!("Failed to Add DNS Record. Status Code: {status_code}. {err}"); + + event!(Level::ERROR, message); + Self { + message, + status_code: StatusCode::INTERNAL_SERVER_ERROR, + source: None, + } } } diff --git a/src/modules/cloudflare/remove_dns_record.rs b/src/modules/cloudflare/remove_dns_record.rs index 275019a..7a3b5e1 100644 --- a/src/modules/cloudflare/remove_dns_record.rs +++ b/src/modules/cloudflare/remove_dns_record.rs @@ -1,3 +1,4 @@ +use axum::http::StatusCode; use serde_json::Value; use tracing::{event, Level}; @@ -5,11 +6,13 @@ use crate::configs::environment::{CLOUDFLARE_ZONE, DEVELOPMENT}; use crate::modules::cloudflare::types::cloudflare_responses::CloudflareError; use crate::modules::cloudflare::types::delete_dns_record::{Failure, Success}; use crate::modules::cloudflare::CLOUDFLARE_CLIENT; +use crate::types::other::voyager_error::VoyagerError; use crate::utils::http_client::ensure_success::EnsureSuccess; +use crate::utils::Error; -pub async fn remove_dns_record(dns_record: &str) -> Option<()> { +pub async fn remove_dns_record(dns_record: &str) -> Result<(), VoyagerError> { if *DEVELOPMENT { - return Some(()); + return Ok(()); } event!( @@ -23,24 +26,17 @@ pub async fn remove_dns_record(dns_record: &str) -> Option<()> { CLOUDFLARE_ZONE.clone(), dns_record ); - let (is_success, response, status) = CLOUDFLARE_CLIENT + + let (response, status_code) = CLOUDFLARE_CLIENT .write() .await .delete::(route.as_str(), Some(&dns_record)) .await - .ensure_success(false); - if !is_success { - event!( - Level::ERROR, - "Failed to send request to Add DNS Record with Cloudflare." - ); - return None; - } + .ensure_success(false) + .map_err(|e| VoyagerError::cloudflare_remove_req(Box::new(e)))?; // These are already checked by the .ensure_success(false) + is_success checks above #[allow(clippy::unwrap_used)] let response = response.unwrap().data().unwrap(); - #[allow(clippy::unwrap_used)] - let status = status.unwrap(); event!(Level::DEBUG, "Request sent to Cloudflare"); @@ -52,34 +48,54 @@ pub async fn remove_dns_record(dns_record: &str) -> Option<()> { "Cloudflare request was successful with id: {}", id ); - Some(()) + Ok(()) } else { - let failure = serde_json::from_value::(response); - let failure = match failure { - Ok(failure) => failure, - Err(err) => { - event!( - Level::ERROR, - "Failed to deserialize failed response for Cloudflare. Status was: {}. Error: {}", - status, - err - ); - return None; - } - }; + let failure = serde_json::from_value::(response) + .map_err(|e| VoyagerError::cloudflare_remove_deserialize(Box::new(e), status_code))?; + + Err(VoyagerError::cloudflare_remove_failure( + &failure, + status_code, + )) + } +} + +impl VoyagerError { + fn cloudflare_remove_req(e: Error) -> Self { + let message = format!("Failed to send Remove DNS request to Cloudflare. Error: {e}"); + event!(Level::ERROR, message); + Self { + message, + status_code: StatusCode::INTERNAL_SERVER_ERROR, + source: Some(e), + } + } + fn cloudflare_remove_deserialize(e: Error, status_code: reqwest::StatusCode) -> Self { + let message = + format!("Failed to deserialize Remove DNS request response from Cloudflare. Status was: {status_code}. Error: {e}"); + event!(Level::ERROR, message); + Self { + message, + status_code: StatusCode::INTERNAL_SERVER_ERROR, + source: Some(e), + } + } + + fn cloudflare_remove_failure(failure: &Failure, status_code: reqwest::StatusCode) -> Self { let err = failure .errors .iter() .fold(String::from("Cloudflare Errors:"), |acc, e| { format!("{acc}\n{e}") }); - event!( - Level::ERROR, - "Request failed with status {status}. Failed to remove DNS record. {}", - err - ); + let message = format!("Failed to Remove DNS record. Status Code: {status_code}. {err}"); - None + event!(Level::ERROR, message); + Self { + message, + status_code: StatusCode::INTERNAL_SERVER_ERROR, + source: None, + } } } diff --git a/src/modules/discord/send_deployment_message.rs b/src/modules/discord/send_deployment_message.rs index 9ef7f9f..84e5477 100644 --- a/src/modules/discord/send_deployment_message.rs +++ b/src/modules/discord/send_deployment_message.rs @@ -1,3 +1,4 @@ +use axum::http::StatusCode; use serenity::all::Webhook; use serenity::builder::CreateEmbed; use serenity::builder::ExecuteWebhook; @@ -9,11 +10,17 @@ use crate::configs::environment::DEVELOPMENT; use crate::configs::environment::DISCORD_WEBHOOK; use crate::types::model::deployment::Deployment; use crate::types::model::deployment::Mode; +use crate::types::other::voyager_error::VoyagerError; use crate::utils::Error; -pub async fn send_deployment_message(id: &str, name: &str, host: &str, mode: &Mode) -> Option<()> { +pub async fn send_deployment_message( + id: &str, + name: &str, + host: &str, + mode: &Mode, +) -> Result<(), VoyagerError> { if *DEVELOPMENT { - return Some(()); + return Ok(()); } event!( @@ -31,27 +38,41 @@ pub async fn send_deployment_message(id: &str, name: &str, host: &str, mode: &Mo let builder = ExecuteWebhook::new().username("Voyager API").embed(embed); let http = Http::new(""); - let webhook_client = Webhook::from_url(&http, &DISCORD_WEBHOOK).await; - match webhook_client { - Ok(webhook_client) => match webhook_client.execute(&http, false, builder).await { - Ok(msg) => { - let message = msg.map_or(String::new(), |msg| { - format!(" Returned message is: {}", msg.content) - }); - event!(Level::INFO, "Discord webhook sent successfuly!{}", message); - } - Err(e) => { - event!(Level::ERROR, "Error sending webhook discord message: {}", e); - } - }, - Err(e) => { - event!( - Level::ERROR, - "Discord webhook client was not created! {}", - e - ); + let webhook_client = Webhook::from_url(&http, &DISCORD_WEBHOOK) + .await + .map_err(|e| VoyagerError::create_discord_client(Box::new(e)))?; + + let msg = webhook_client + .execute(&http, false, builder) + .await + .map_err(|e| VoyagerError::execute_discord_webhook(Box::new(e)))?; + + let message = msg.map_or(String::new(), |msg| { + format!("Returned message is: {}", msg.content) + }); + event!(Level::INFO, "Discord webhook sent successfuly! {}", message); + + Ok(()) +} + +impl VoyagerError { + fn create_discord_client(e: Error) -> Self { + let message = format!("Failed to create Discord client: {e}"); + event!(Level::ERROR, message); + Self { + message, + status_code: StatusCode::INTERNAL_SERVER_ERROR, + source: Some(e), } } - Some(()) + fn execute_discord_webhook(e: Error) -> Self { + let message = format!("Failed to send Discord webhook: {e}"); + event!(Level::ERROR, message); + Self { + message, + status_code: StatusCode::INTERNAL_SERVER_ERROR, + source: Some(e), + } + } } diff --git a/src/modules/docker/build_image.rs b/src/modules/docker/build_image.rs index 96857ad..83cd2cb 100644 --- a/src/modules/docker/build_image.rs +++ b/src/modules/docker/build_image.rs @@ -18,7 +18,7 @@ pub async fn build_image( ) -> Result { let dockerfile_str = dockerfile .to_str() - .ok_or_else(|| VoyagerError::path_to_string())? + .ok_or_else(VoyagerError::path_to_string)? .to_string(); let options = BuildImageOptions { @@ -76,21 +76,21 @@ pub async fn build_image( } impl VoyagerError { - pub fn build_image() -> Self { - let message = format!("Failed to build image! Image Id was empty."); + fn build_image() -> Self { + let message = "Failed to build image! Image Id was empty."; event!(Level::ERROR, message); - VoyagerError { - message, + Self { + message: message.to_string(), status_code: StatusCode::INTERNAL_SERVER_ERROR, source: None, } } - pub fn path_to_string() -> Self { - let message = format!("Failed to convert Path to String!"); + fn path_to_string() -> Self { + let message = "Failed to convert Path to String!"; event!(Level::ERROR, message); - VoyagerError { - message, + Self { + message: message.to_string(), status_code: StatusCode::INTERNAL_SERVER_ERROR, source: None, } diff --git a/src/modules/docker/create_container.rs b/src/modules/docker/create_container.rs index c738733..02cdf8e 100644 --- a/src/modules/docker/create_container.rs +++ b/src/modules/docker/create_container.rs @@ -64,10 +64,10 @@ pub async fn create_container( } impl VoyagerError { - pub fn create_container(e: Error) -> Self { + fn create_container(e: Error) -> Self { let message = format!("Failed to create container! Error: {e}"); event!(Level::ERROR, message); - VoyagerError { + Self { message, status_code: StatusCode::INTERNAL_SERVER_ERROR, source: Some(e), diff --git a/src/modules/docker/delete_container.rs b/src/modules/docker/delete_container.rs index 15b8d62..dfa4233 100644 --- a/src/modules/docker/delete_container.rs +++ b/src/modules/docker/delete_container.rs @@ -15,7 +15,7 @@ pub async fn delete_container(container_name: String) -> Result<(), VoyagerError link: false, }); - let result = DOCKER_RUNTIME + DOCKER_RUNTIME .spawn_handled("modules::docker::delete_container", async move { DOCKER.remove_container(&container_name, options).await }) @@ -28,10 +28,10 @@ pub async fn delete_container(container_name: String) -> Result<(), VoyagerError } impl VoyagerError { - pub fn delete_container(e: Error) -> Self { + fn delete_container(e: Error) -> Self { let message = format!("Failed to delete container! Error: {e}"); event!(Level::ERROR, message); - VoyagerError { + Self { message, status_code: StatusCode::INTERNAL_SERVER_ERROR, source: Some(e), diff --git a/src/modules/docker/delete_image.rs b/src/modules/docker/delete_image.rs index b20cb44..71fe2e7 100644 --- a/src/modules/docker/delete_image.rs +++ b/src/modules/docker/delete_image.rs @@ -28,10 +28,10 @@ pub async fn delete_image(image_name: String) -> Result<(), VoyagerError> { } impl VoyagerError { - pub fn remove_image(e: Error) -> Self { + fn remove_image(e: Error) -> Self { let message = format!("Failed to remove image! Error: {e}"); event!(Level::ERROR, message); - VoyagerError { + Self { message, status_code: StatusCode::INTERNAL_SERVER_ERROR, source: Some(e), diff --git a/src/modules/docker/get_internal_port.rs b/src/modules/docker/get_internal_port.rs index 079f522..9f9be9a 100644 --- a/src/modules/docker/get_internal_port.rs +++ b/src/modules/docker/get_internal_port.rs @@ -22,10 +22,10 @@ pub fn find_internal_port(docker_file_content: &str) -> Result Self { + fn parse_port() -> Self { let message = "Failed to parse internal port from Dockerfile".to_string(); event!(Level::ERROR, message); - VoyagerError { + Self { message, status_code: StatusCode::INTERNAL_SERVER_ERROR, source: None, diff --git a/src/modules/docker/is_container_running.rs b/src/modules/docker/is_container_running.rs index c0f96e4..c57cb15 100644 --- a/src/modules/docker/is_container_running.rs +++ b/src/modules/docker/is_container_running.rs @@ -25,7 +25,7 @@ pub async fn is_container_running(container_name: String) -> Result Result Self { + fn inspect_container(e: Error) -> Self { let message = format!("Failed to inspect container! Error: {e}"); event!(Level::ERROR, message); - VoyagerError { + Self { message, status_code: StatusCode::INTERNAL_SERVER_ERROR, source: Some(e), } } - pub fn empty_state() -> Self { - let message = "State was None! Failed to get if container is running.".to_string(); + fn empty_state() -> Self { + let message = "State was None! Failed to get if container is running."; event!(Level::ERROR, message); - VoyagerError { - message, + Self { + message: message.to_string(), status_code: StatusCode::INTERNAL_SERVER_ERROR, source: None, } diff --git a/src/modules/docker/restart_container.rs b/src/modules/docker/restart_container.rs index d64d1cf..8e534fe 100644 --- a/src/modules/docker/restart_container.rs +++ b/src/modules/docker/restart_container.rs @@ -15,12 +15,12 @@ pub async fn restart_container(container_name: String) -> Result<(), VoyagerErro container_name ); - let result = DOCKER_RUNTIME + DOCKER_RUNTIME .spawn_handled("modules::docker::restart_container", async move { DOCKER.restart_container(&container_name, None).await }) .await? - .map_err(|e| VoyagerError::stop_container(Box::new(e)))?; + .map_err(|e| VoyagerError::restart_container(Box::new(e)))?; event!(Level::DEBUG, "Done restarting container."); @@ -28,10 +28,10 @@ pub async fn restart_container(container_name: String) -> Result<(), VoyagerErro } impl VoyagerError { - pub fn restart_container(e: Error) -> Self { + fn restart_container(e: Error) -> Self { let message = format!("Failed to restart container! Error: {e}"); event!(Level::ERROR, message); - VoyagerError { + Self { message, status_code: StatusCode::INTERNAL_SERVER_ERROR, source: Some(e), diff --git a/src/modules/docker/start_container.rs b/src/modules/docker/start_container.rs index 9a2d4b4..65f3150 100644 --- a/src/modules/docker/start_container.rs +++ b/src/modules/docker/start_container.rs @@ -30,10 +30,10 @@ pub async fn start_container(container_name: String) -> Result<(), VoyagerError> } impl VoyagerError { - pub fn start_container(e: Error) -> Self { + fn start_container(e: Error) -> Self { let message = format!("Failed to start container! Error: {e}"); event!(Level::ERROR, message); - VoyagerError { + Self { message, status_code: StatusCode::INTERNAL_SERVER_ERROR, source: Some(e), diff --git a/src/modules/docker/stop_container.rs b/src/modules/docker/stop_container.rs index 4c9383d..91cb141 100644 --- a/src/modules/docker/stop_container.rs +++ b/src/modules/docker/stop_container.rs @@ -27,10 +27,10 @@ pub async fn stop_container(container_name: String) -> Result<(), VoyagerError> } impl VoyagerError { - pub fn stop_container(e: Error) -> Self { + fn stop_container(e: Error) -> Self { let message = format!("Failed to stop container! Error: {e}"); event!(Level::ERROR, message); - VoyagerError { + Self { message, status_code: StatusCode::INTERNAL_SERVER_ERROR, source: Some(e), diff --git a/src/types/to_json_str.rs b/src/types/to_json_str.rs index f3f2afe..64d220a 100644 --- a/src/types/to_json_str.rs +++ b/src/types/to_json_str.rs @@ -1,12 +1,23 @@ +use axum::http::StatusCode; use serde::Serialize; use tracing::{event, Level}; -pub fn to_json_str(obj: &(impl Serialize + std::fmt::Debug)) -> Option { - match serde_json::to_string(obj) { - Ok(jsonstr) => Some(jsonstr), - Err(err) => { - event!(Level::ERROR, "Failed to convert {:?} to json string", obj); - None +use crate::utils::Error; + +use super::other::voyager_error::VoyagerError; + +pub fn to_json_str(obj: &(impl Serialize + std::fmt::Debug)) -> Result { + serde_json::to_string(obj).map_err(|e| VoyagerError::to_json_str(obj, Box::new(e))) +} + +impl VoyagerError { + pub fn to_json_str(obj: &(impl Serialize + std::fmt::Debug), e: Error) -> Self { + let message = format!("Failed to convert {obj:?} to json string! Error: {e}"); + event!(Level::ERROR, message); + Self { + message, + status_code: StatusCode::INTERNAL_SERVER_ERROR, + source: Some(e), } } } diff --git a/src/utils/get_free_port.rs b/src/utils/get_free_port.rs index 941ed94..9d1e351 100644 --- a/src/utils/get_free_port.rs +++ b/src/utils/get_free_port.rs @@ -1,21 +1,18 @@ use std::net::TcpListener; +use axum::http::StatusCode; use tracing::{event, Level}; -use crate::{configs::environment::HOST_IP, utils::Error}; +use crate::{ + configs::environment::HOST_IP, types::other::voyager_error::VoyagerError, utils::Error, +}; -pub fn get_free_port() -> Option { +pub fn get_free_port() -> Result { event!(Level::INFO, "Attempting to get free port"); - match _get_free_port() { - Ok(port) => { - event!(Level::INFO, "Succcessfully Got free port: {}", port); - Some(port) - } - Err(e) => { - event!(Level::ERROR, "Failed to get free port: {}", e); - None - } - } + let port = _get_free_port().map_err(VoyagerError::get_free_port)?; + + event!(Level::INFO, "Succcessfully got free port: {port}"); + Ok(port) } fn _get_free_port() -> Result { @@ -25,3 +22,15 @@ fn _get_free_port() -> Result { .port(), ) } + +impl VoyagerError { + fn get_free_port(e: Error) -> Self { + let message = format!("Failed to get free port: {e}"); + event!(Level::ERROR, message); + Self { + message, + status_code: StatusCode::INTERNAL_SERVER_ERROR, + source: Some(e), + } + } +} diff --git a/src/utils/http_client/deserializable.rs b/src/utils/http_client/deserializable.rs index 471dc40..87891a5 100644 --- a/src/utils/http_client/deserializable.rs +++ b/src/utils/http_client/deserializable.rs @@ -1,5 +1,6 @@ use serde_json::Value; +#[derive(Debug)] pub enum Deserializable serde::Deserialize<'de>> { Value(Value), Data(T), diff --git a/src/utils/http_client/ensure_success.rs b/src/utils/http_client/ensure_success.rs index fb5157f..4aa8954 100644 --- a/src/utils/http_client/ensure_success.rs +++ b/src/utils/http_client/ensure_success.rs @@ -1,48 +1,46 @@ -use crate::utils::http_client::deserializable::Deserializable; -use crate::utils::http_client::Response; -use crate::utils::Error; use reqwest::StatusCode; -use tracing::{event, Level}; -pub trait EnsureSuccess serde::Deserialize<'de>> { +use super::{deserializable::Deserializable, http_error::HttpError, Response}; +use crate::utils::Error as OurErr; + +pub trait EnsureSuccess serde::Deserialize<'de>> { fn ensure_success( self, is_nullable: bool, - ) -> (bool, Option>, Option); + ) -> Result<(Option>, StatusCode), HttpError>; } -impl serde::Deserialize<'de>> EnsureSuccess for Result, Error> { +impl serde::Deserialize<'de>> EnsureSuccess + for Result, OurErr> +{ fn ensure_success( self, is_nullable: bool, - ) -> (bool, Option>, Option) { - match self { - Ok(response) => { - let (res, status_code) = response; - - if !status_code.is_success() { - event!( - Level::ERROR, - "Status Code: HTTP {status_code}. Response returned error" - ); - (false, res, Some(status_code)) - } else if !is_nullable && res.is_none() { - event!( - Level::ERROR, - "Status Code: HTTP {status_code}. Response body was empty on non-nullable entity" - ); - (false, res, Some(status_code)) - } else { - (true, res, Some(status_code)) - } - } + ) -> Result<(Option>, StatusCode), HttpError> { + let (res, status_code) = self.map_err(|e| { + HttpError::new( + "HTTP CLient failed to send request".to_string(), + None, + None, + Some(e), + ) + })?; - Err(e) => { - event!( - Level::ERROR, - "HTTP Client failed to contact the API. Error: {e}" - ); - (false, None, None) - } + if !status_code.is_success() { + Err(HttpError::new( + "Response returned error".to_string(), + Some(status_code), + res, + None, + )) + } else if !is_nullable && res.is_none() { + Err(HttpError::new( + "Response body was empty on non-nullable entity".to_string(), + Some(status_code), + res, + None, + )) + } else { + Ok((res, status_code)) } } } diff --git a/src/utils/http_client/http_error.rs b/src/utils/http_client/http_error.rs new file mode 100644 index 0000000..ff9ec32 --- /dev/null +++ b/src/utils/http_client/http_error.rs @@ -0,0 +1,55 @@ +use super::deserializable::Deserializable; +use crate::utils::Error as OurErr; +use reqwest::StatusCode; +use std::{error::Error, fmt}; +use tracing::{event, Level}; + +#[derive(Debug)] +pub struct HttpError serde::Deserialize<'de>> { + pub message: String, + pub status_code: Option, + pub response: Option>, + pub source: Option, +} + +impl serde::Deserialize<'de>> fmt::Display for HttpError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let message = self.status_code.map_or_else( + || self.message.clone(), + |status_code| format!("Status Code: HTTP {status_code}. {}", self.message), + ); + + let message = self.status_code.map_or_else( + || self.message.clone(), + |status_code| format!("Status Code: HTTP {status_code}. {}", self.message), + ); + + write!(f, "{message}") + } +} + +impl serde::Deserialize<'de>> Error for HttpError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + self.source.as_deref().map(|s| s as _) + } +} + +impl serde::Deserialize<'de>> HttpError { + pub fn new( + message: String, + status_code: Option, + response: Option>, + source: Option, + ) -> Self { + let result = Self { + message, + status_code, + response, + source, + }; + + event!(Level::ERROR, "{result}"); + + result + } +} diff --git a/src/utils/http_client/mod.rs b/src/utils/http_client/mod.rs index b42b1fd..d9b12ac 100644 --- a/src/utils/http_client/mod.rs +++ b/src/utils/http_client/mod.rs @@ -2,6 +2,7 @@ mod client_wrapper; pub mod deserializable; pub mod ensure_success; mod generate_methods; +mod http_error; use crate::utils::http_client::client_wrapper::ClientWrapper; use crate::utils::http_client::deserializable::Deserializable; diff --git a/src/utils/runtime_helpers.rs b/src/utils/runtime_helpers.rs index 2a0a071..8243121 100644 --- a/src/utils/runtime_helpers.rs +++ b/src/utils/runtime_helpers.rs @@ -27,9 +27,9 @@ impl RuntimeSpawnHandled for Runtime { impl VoyagerError { pub fn spawn(task: &str, e: Error) -> Self { - let message = format!("Failed to complete task '{}'! Error: {e}", task); + let message = format!("Failed to complete task '{task}'! Error: {e}"); event!(Level::ERROR, message); - VoyagerError { + Self { message, status_code: StatusCode::INTERNAL_SERVER_ERROR, source: Some(e),