diff --git a/src/cloud_provider/aws/kubernetes/helm_charts.rs b/src/cloud_provider/aws/kubernetes/helm_charts.rs index 4457d9a9..b334e968 100644 --- a/src/cloud_provider/aws/kubernetes/helm_charts.rs +++ b/src/cloud_provider/aws/kubernetes/helm_charts.rs @@ -6,7 +6,7 @@ use crate::cloud_provider::helm::{ }; use crate::cloud_provider::qovery::{get_qovery_app_version, EngineLocation, QoveryAgent, QoveryAppName, QoveryEngine}; use crate::cmd::kubectl::{kubectl_delete_crash_looping_pods, kubectl_exec_get_daemonset, kubectl_exec_with_output}; -use crate::errors::CommandError; +use crate::errors::{CommandError, CommandErrorMessageVerbosity}; use semver::Version; use serde::{Deserialize, Serialize}; use std::fs::File; @@ -66,10 +66,10 @@ pub fn aws_helm_charts( let content_file = match File::open(&qovery_terraform_config_file) { Ok(x) => x, Err(e) => { - let message_safe = "Can't deploy helm chart as Qovery terraform config file has not been rendered by Terraform. Are you running it in dry run mode?"; return Err(CommandError::new( - format!("{}, error: {:?}", message_safe.to_string(), e), - Some(message_safe.to_string()), + "Can't deploy helm chart as Qovery terraform config file has not been rendered by Terraform. Are you running it in dry run mode?".to_string(), + Some(e.to_string()), + true, )); } }; @@ -79,13 +79,13 @@ pub fn aws_helm_charts( let qovery_terraform_config: AwsQoveryTerraformConfig = match serde_json::from_reader(reader) { Ok(config) => config, Err(e) => { - let message_safe = format!( - "Error while parsing terraform config file {}", - qovery_terraform_config_file - ); return Err(CommandError::new( - format!("{}, error: {:?}", message_safe.to_string(), e), - Some(message_safe.to_string()), + format!( + "Error while parsing terraform config file {}", + qovery_terraform_config_file + ), + Some(e.to_string()), + true, )); } }; @@ -1344,16 +1344,14 @@ impl AwsVpcCniChart { _ => Ok(false), }, }, - Err(e) => { - let message_safe = format!( + Err(e) => Err(CommandError::new( + format!( "Error while getting daemonset info for chart {}, won't deploy CNI chart.", &self.chart_info.name - ); - Err(CommandError::new( - format!("{}, error: {:?}", message_safe, e), - Some(message_safe), - )) - } + ), + Some(e.message(CommandErrorMessageVerbosity::FullDetails)), + true, + )), } } diff --git a/src/cloud_provider/aws/kubernetes/mod.rs b/src/cloud_provider/aws/kubernetes/mod.rs index 0c698c54..89ce0c47 100644 --- a/src/cloud_provider/aws/kubernetes/mod.rs +++ b/src/cloud_provider/aws/kubernetes/mod.rs @@ -33,7 +33,7 @@ use crate::cmd::terraform::{terraform_exec, terraform_init_validate_plan_apply, use crate::deletion_utilities::{get_firsts_namespaces_to_delete, get_qovery_managed_namespaces}; use crate::dns_provider; use crate::dns_provider::DnsProvider; -use crate::errors::{CommandError, EngineError}; +use crate::errors::{CommandError, CommandErrorMessageVerbosity, EngineError}; use crate::events::Stage::Infrastructure; use crate::events::{EngineEvent, EnvironmentStep, EventDetails, EventMessage, InfrastructureStep, Stage, Transmitter}; use crate::logger::{LogLevel, Logger}; @@ -837,7 +837,10 @@ impl<'a> EKS<'a> { LogLevel::Error, EngineEvent::Deploying( event_details.clone(), - EventMessage::new("Error trying to get kubernetes events".to_string(), Some(err.message())), + EventMessage::new( + "Error trying to get kubernetes events".to_string(), + Some(err.message(CommandErrorMessageVerbosity::FullDetails)), + ), ), ), }; @@ -971,10 +974,7 @@ impl<'a> EKS<'a> { for metric in metrics.items { match metric.value.parse::() { Ok(job_count) if job_count > 0 => current_engine_jobs += 1, - Err(e) => { - let safe_message = "Error while looking at the API metric value"; - return OperationResult::Retry(EngineError::new_cannot_get_k8s_api_custom_metrics(event_details.clone(), CommandError::new(format!("{}, error: {}", safe_message, e.to_string()), Some(safe_message.to_string())))); - } + Err(e) => OperationResult::Retry(EngineError::new_cannot_get_k8s_api_custom_metrics(event_details.clone(), CommandError::new("Error while looking at the API metric value".to_string(), Some(e.to_string()), true))), _ => {} } } @@ -1108,12 +1108,14 @@ impl<'a> EKS<'a> { let kubernetes_config_file_path = match self.get_kubeconfig_file_path() { Ok(x) => x, Err(e) => { - let safe_message = "Skipping Kubernetes uninstall because it can't be reached."; self.logger().log( LogLevel::Warning, EngineEvent::Deleting( event_details.clone(), - EventMessage::new(safe_message.to_string(), Some(e.message())), + EventMessage::new( + "Skipping Kubernetes uninstall because it can't be reached.".to_string(), + Some(e.message(CommandErrorMessageVerbosity::FullDetails)), + ), ), ); @@ -1201,7 +1203,11 @@ impl<'a> EKS<'a> { ), ), Err(e) => { - if !(e.message().contains("not found")) { + if !(e + .message(CommandErrorMessageVerbosity::FullDetails) + .to_lowercase() + .contains("not found")) + { self.logger().log( LogLevel::Error, EngineEvent::Deleting( @@ -1218,15 +1224,17 @@ impl<'a> EKS<'a> { } } Err(e) => { - let message_safe = format!( - "Error while getting all namespaces for Kubernetes cluster {}", - self.name_with_id(), - ); self.logger().log( LogLevel::Error, EngineEvent::Deleting( event_details.clone(), - EventMessage::new(message_safe, Some(e.message())), + EventMessage::new( + format!( + "Error while getting all namespaces for Kubernetes cluster {}", + self.name_with_id(), + ), + Some(e.message(CommandErrorMessageVerbosity::FullDetails)), + ), ), ); } @@ -1299,7 +1307,10 @@ impl<'a> EKS<'a> { LogLevel::Error, EngineEvent::Deleting( event_details.clone(), - EventMessage::new(message_safe, Some(e.message())), + EventMessage::new( + message_safe, + Some(e.message(CommandErrorMessageVerbosity::FullDetails)), + ), ), ) } @@ -1307,7 +1318,11 @@ impl<'a> EKS<'a> { } } Err(e) => { - if !(e.message().contains("not found")) { + if !(e + .message(CommandErrorMessageVerbosity::FullDetails) + .to_lowercase() + .contains("not found")) + { self.logger().log( LogLevel::Error, EngineEvent::Deleting( @@ -1346,7 +1361,11 @@ impl<'a> EKS<'a> { ), ), Err(e) => { - if !(e.message().contains("not found")) { + if !(e + .message(CommandErrorMessageVerbosity::FullDetails) + .to_lowercase() + .contains("not found")) + { self.logger().log( LogLevel::Error, EngineEvent::Deleting( @@ -1402,16 +1421,16 @@ impl<'a> EKS<'a> { } } } - Err(e) => { - let message_safe = "Unable to get helm list"; - self.logger().log( - LogLevel::Error, - EngineEvent::Deleting( - event_details.clone(), - EventMessage::new(message_safe.to_string(), Some(e.message())), + Err(e) => self.logger().log( + LogLevel::Error, + EngineEvent::Deleting( + event_details.clone(), + EventMessage::new( + "Unable to get helm list".to_string(), + Some(e.message(CommandErrorMessageVerbosity::FullDetails)), ), - ) - } + ), + ), } }; @@ -1456,7 +1475,7 @@ impl<'a> EKS<'a> { )), Err(retry::Error::Internal(msg)) => Err(EngineError::new_terraform_error_while_executing_destroy_pipeline( event_details.clone(), - CommandError::new(msg, None), + CommandError::new_from_safe_message(msg), )), } } diff --git a/src/cloud_provider/aws/kubernetes/node.rs b/src/cloud_provider/aws/kubernetes/node.rs index ea3e8d47..a8424094 100644 --- a/src/cloud_provider/aws/kubernetes/node.rs +++ b/src/cloud_provider/aws/kubernetes/node.rs @@ -66,8 +66,10 @@ impl FromStr for AwsInstancesType { "t3a.large" => Ok(AwsInstancesType::T3aLarge), "t3a.2xlarge" => Ok(AwsInstancesType::T3a2xlarge), _ => { - let message = format!("`{}` instance type is not supported", s); - return Err(CommandError::new(message.clone(), Some(message))); + return Err(CommandError::new_from_safe_message(format!( + "`{}` instance type is not supported", + s + ))); } } } diff --git a/src/cloud_provider/aws/kubernetes/roles.rs b/src/cloud_provider/aws/kubernetes/roles.rs index e4593425..68371639 100644 --- a/src/cloud_provider/aws/kubernetes/roles.rs +++ b/src/cloud_provider/aws/kubernetes/roles.rs @@ -43,8 +43,9 @@ impl Role { match role { Ok(_) => Ok(true), Err(e) => Err(CommandError::new( - format!("Unable to know if {} exist on AWS account: {:?}", &self.role_name, e), - Some(format!("Unable to know if {} exist on AWS account.", &self.role_name,)), + format!("Unable to know if {} exist on AWS account.", &self.role_name,), + Some(e.to_string()), + true, )), } } @@ -78,10 +79,10 @@ impl Role { return match created { Ok(_) => Ok(true), Err(e) => { - let safe_message = format!("Unable to know if `{}` exist on AWS Account", &self.role_name); return Err(CommandError::new( - format!("{}, error: {:?}", safe_message, e), - Some(safe_message), + format!("Unable to know if `{}` exist on AWS Account", &self.role_name), + Some(e.to_string()), + true, )); } }; diff --git a/src/cloud_provider/digitalocean/do_api_common.rs b/src/cloud_provider/digitalocean/do_api_common.rs index c98ab564..d8302bc8 100644 --- a/src/cloud_provider/digitalocean/do_api_common.rs +++ b/src/cloud_provider/digitalocean/do_api_common.rs @@ -33,28 +33,26 @@ pub fn do_get_from_api(token: &str, api_type: DoApiType, url_api: String) -> Res let res = reqwest::blocking::Client::new().get(url_api).headers(headers).send(); match res { - Ok(response) => { - match response.status() { - StatusCode::OK => Ok(response.text().expect("Cannot get response text")), - StatusCode::UNAUTHORIZED => { - let message_safe = format!( + Ok(response) => match response.status() { + StatusCode::OK => Ok(response.text().expect("Cannot get response text")), + StatusCode::UNAUTHORIZED => { + return Err(CommandError::new( + format!( "Could not get {} information, ensure your DigitalOcean token is valid.", api_type - ); - return Err(CommandError::new( - format!("{}, response: {:?}", message_safe.to_string(), response), - Some(message_safe.to_string()), - )); - } - _ => { - let message_safe = format!("Unknown status code received from Digital Ocean Kubernetes API while retrieving {} information.", api_type); - return Err(CommandError::new( - format!("{}, response: {:?}", message_safe.to_string(), response), - Some(message_safe.to_string()), - )); - } + ), + Some(format!("response: {:?}", response)), + true, + )); } - } + _ => { + return Err(CommandError::new( + format!("Unknown status code received from Digital Ocean Kubernetes API while retrieving {} information.", api_type), + Some(format!("Response: {:?}", response)), + true, + )); + } + }, Err(_) => Err(CommandError::new_from_safe_message(format!( "Unable to get a response from Digital Ocean {} API", api_type diff --git a/src/cloud_provider/digitalocean/kubernetes/doks_api.rs b/src/cloud_provider/digitalocean/kubernetes/doks_api.rs index 56aad133..c4b0e801 100644 --- a/src/cloud_provider/digitalocean/kubernetes/doks_api.rs +++ b/src/cloud_provider/digitalocean/kubernetes/doks_api.rs @@ -25,10 +25,10 @@ pub fn get_doks_info_from_name( Ok(cluster_info) } Err(e) => { - let safe_message = "Error while trying to deserialize json received from Digital Ocean DOKS API"; return Err(CommandError::new( - format!("{}, error: {}", safe_message.to_string(), e.to_string()), - Some(safe_message.to_string()), + "Error while trying to deserialize json received from Digital Ocean DOKS API".to_string(), + Some(e.to_string()), + true, )); } } @@ -49,10 +49,10 @@ fn get_doks_versions_from_api_output(json_content: &str) -> Result Ok(options.options.versions), Err(e) => { - let safe_message = "Error while trying to deserialize json received from Digital Ocean DOKS API"; return Err(CommandError::new( - format!("{}, error: {}", safe_message.to_string(), e.to_string()), - Some(safe_message.to_string()), + "Error while trying to deserialize json received from Digital Ocean DOKS API".to_string(), + Some(e.to_string()), + true, )); } } diff --git a/src/cloud_provider/digitalocean/kubernetes/helm_charts.rs b/src/cloud_provider/digitalocean/kubernetes/helm_charts.rs index aeadf32e..0c2d26c3 100644 --- a/src/cloud_provider/digitalocean/kubernetes/helm_charts.rs +++ b/src/cloud_provider/digitalocean/kubernetes/helm_charts.rs @@ -121,10 +121,11 @@ pub fn do_helm_charts( let content_file = match File::open(&qovery_terraform_config_file) { Ok(x) => x, Err(e) => { - let message_safe = "Can't deploy helm chart as Qovery terraform config file has not been rendered by Terraform. Are you running it in dry run mode?"; + let message_safe = return Err(CommandError::new( - format!("{}, error: {:?}", message_safe.to_string(), e), - Some(message_safe.to_string()), + "Can't deploy helm chart as Qovery terraform config file has not been rendered by Terraform. Are you running it in dry run mode?".to_string(), + Some(e.to_string()), + true, )); } }; @@ -134,13 +135,13 @@ pub fn do_helm_charts( let qovery_terraform_config: DigitalOceanQoveryTerraformConfig = match serde_json::from_reader(reader) { Ok(config) => config, Err(e) => { - let message_safe = format!( - "Error while parsing terraform config file {}", - qovery_terraform_config_file - ); return Err(CommandError::new( - format!("{}, error: {:?}", message_safe.to_string(), e), - Some(message_safe.to_string()), + format!( + "Error while parsing terraform config file {}", + qovery_terraform_config_file + ), + Some(e.to_string()), + true, )); } }; diff --git a/src/cloud_provider/digitalocean/kubernetes/node.rs b/src/cloud_provider/digitalocean/kubernetes/node.rs index 6e1d957b..549d374a 100644 --- a/src/cloud_provider/digitalocean/kubernetes/node.rs +++ b/src/cloud_provider/digitalocean/kubernetes/node.rs @@ -71,8 +71,10 @@ impl FromStr for DoInstancesType { "s-6vcpu-16gb" => Ok(DoInstancesType::S6vcpu16gb), "s-8vcpu-32gb" => Ok(DoInstancesType::S8vcpu32gb), _ => { - let message = format!("`{}` instance type is not supported", s); - return Err(CommandError::new(message.clone(), Some(message))); + return Err(CommandError::new_from_safe_message(format!( + "`{}` instance type is not supported", + s + ))); } } } diff --git a/src/errors/io.rs b/src/errors/io.rs index 139b5df7..b3c5902d 100644 --- a/src/errors/io.rs +++ b/src/errors/io.rs @@ -12,8 +12,8 @@ pub struct CommandError { impl From for CommandError { fn from(error: errors::CommandError) -> Self { CommandError { - message: error.message_safe.unwrap_or("".to_string()), - message_unsafe: error.message_raw, + message: error.safe_message.unwrap_or("".to_string()), + message_unsafe: error.full_details, } } } diff --git a/src/errors/mod.rs b/src/errors/mod.rs index 47e0ac44..1a8acd81 100644 --- a/src/errors/mod.rs +++ b/src/errors/mod.rs @@ -7,34 +7,48 @@ use crate::error::{EngineError as LegacyEngineError, EngineErrorCause, EngineErr use crate::events::EventDetails; use url::Url; +pub enum CommandErrorMessageVerbosity { + SafeOnly, + FullDetails, +} + /// CommandError: command error, mostly returned by third party tools. #[derive(Clone, Debug, PartialEq)] pub struct CommandError { - /// message: full error message, can contains unsafe text such as passwords and tokens. - message_raw: String, - /// message_safe: error message omitting displaying any protected data such as passwords and tokens. - message_safe: Option, + /// safe_message: error message omitting displaying any protected data such as passwords and tokens. + safe_message: String, + /// full_details: full error message, can contains unsafe text such as passwords and tokens. + full_details: Option, } impl CommandError { - /// Returns CommandError message_raw. May contains unsafe text such as passwords and tokens. - pub fn message_raw(&self) -> String { - self.message_raw.to_string() + /// Returns CommandError full_details. May contains unsafe text such as passwords and tokens. + pub fn full_details(&self) -> Option { + self.full_details.clone() } - /// Returns CommandError message_safe omitting all unsafe text such as passwords and tokens. - pub fn message_safe(&self) -> Option { - self.message_safe.clone() + /// Returns CommandError safe_message omitting all unsafe text such as passwords and tokens. + pub fn safe_message(&self) -> String { + self.safe_message.to_string() } - /// Returns error all message (safe + unsafe). - pub fn message(&self) -> String { - // TODO(benjaminch): To be revamped, not sure how we should deal with safe and unsafe messages. - if let Some(msg) = &self.message_safe { - return format!("{} {}", msg, self.message_raw); + /// Returns message for command error. + /// + /// Arguments + /// + /// * `message_verbosity`: Which verbosity is required for the message. + pub fn message(&self, message_verbosity: CommandErrorMessageVerbosity) -> String { + match message_verbosity { + CommandErrorMessageVerbosity::SafeOnly => self.safe_message.to_string(), + CommandErrorMessageVerbosity::FullDetails => match &self.full_details { + None => self.safe_message.to_string(), + Some(details) => format!( + "{} / Full error details: {}", + self.safe_message.to_string(), + details.to_string() + ), + }, } - - self.message_raw.to_string() } /// Creates a new CommandError from safe message. To be used when message is safe. @@ -43,10 +57,10 @@ impl CommandError { } /// Creates a new CommandError having both a safe and an unsafe message. - pub fn new(message_raw: String, message_safe: Option) -> Self { + pub fn new(safe_message: String, full_details: Option, refactor: bool) -> Self { CommandError { - message_raw, - message_safe, + full_details, + safe_message, } } @@ -77,7 +91,7 @@ impl CommandError { unsafe_message = format!("{}\nSTDERR {}", unsafe_message, txt); } - CommandError::new(unsafe_message, Some(message)) + CommandError::new(message, Some(unsafe_message), true) } } @@ -235,9 +249,9 @@ impl EngineError { } /// Returns proper error message. - pub fn message(&self) -> String { + pub fn message(&self, message_verbosity: CommandErrorMessageVerbosity) -> String { match &self.message { - Some(msg) => msg.message(), + Some(msg) => msg.message(message_verbosity), None => self.qovery_log_message.to_string(), } } @@ -261,7 +275,7 @@ impl EngineError { /// * `qovery_log_message`: Error log message targeting Qovery team for investigation / monitoring purposes. /// * `user_log_message`: Error log message targeting Qovery user, avoiding any extending pointless details. /// * `error_message`: Raw error message. - /// * `raw_message_safe`: Error raw message such as command input / output where any unsafe data as been omitted (such as plain passwords / tokens). + /// * `raw_safe_message`: Error raw message such as command input / output where any unsafe data as been omitted (such as plain passwords / tokens). /// * `link`: Link documenting the given error. /// * `hint_message`: hint message aiming to give an hint to the user. For example: "Happens when application port has been changed but application hasn't been restarted.". fn new( @@ -290,7 +304,7 @@ impl EngineError { EngineErrorCause::Internal, EngineErrorScope::from(self.event_details.transmitter()), self.event_details.execution_id().to_string(), - Some(self.message()), + Some(self.message(CommandErrorMessageVerbosity::FullDetails)), // Full details verbosity by default for legacy errors ) }