diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 39789f84..7cfb2071 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -24,6 +24,7 @@ tracing = "0.1" file-rotate = "0.7" tracing-appender = "0.2" tracing-subscriber = "0.3" +# tracing-forest = { version = "0.1", features = ["tokio"] } ## Error Handling error-stack = "0.4" diff --git a/backend/src/connector/api.rs b/backend/src/connector/api.rs index 58c3233e..3c9b6a1f 100644 --- a/backend/src/connector/api.rs +++ b/backend/src/connector/api.rs @@ -156,7 +156,6 @@ pub enum GetConfigurationResponse { } /// API -#[async_trait] pub trait Api { fn poll_ready( &self, @@ -226,7 +225,6 @@ pub trait Api { } /// API where `Context` isn't passed on every API call -#[async_trait] pub trait ApiNoContext { fn poll_ready(&self, _cx: &mut Context) -> Poll>; @@ -297,7 +295,6 @@ impl + Send + Sync, C: Clone + Send + Sync> ContextWrapperExt for T } } -#[async_trait] impl + Send + Sync, C: Clone + Send + Sync> ApiNoContext for ContextWrapper { fn poll_ready(&self, cx: &mut Context) -> Poll> { self.api().poll_ready(cx) @@ -404,9 +401,10 @@ impl + Send + Sync, C: Clone + Send + Sync> ApiNoContext for Contex pub mod prelude { pub use super::{ - APIError, Api, GetAlienDirResponse, GetConfigurationResponse, GetDisksResponse, - GetMetricsResponse, GetNodesResponse, GetPartitionResponse, GetPartitionsResponse, - GetRecordsResponse, GetReplicasLocalDirsResponse, GetSpaceInfoResponse, GetStatusResponse, - GetVDiskResponse, GetVDisksResponse, GetVersionResponse, + APIError, Api, GetAlienDirResponse, GetAlienResponse, GetConfigurationResponse, + GetDisksResponse, GetMetricsResponse, GetNodesResponse, GetPartitionResponse, + GetPartitionsResponse, GetRecordsResponse, GetReplicasLocalDirsResponse, + GetSpaceInfoResponse, GetStatusResponse, GetVDiskResponse, GetVDisksResponse, + GetVersionResponse, }; } diff --git a/backend/src/connector/client.rs b/backend/src/connector/client.rs index 3793f24b..ed9d64a4 100644 --- a/backend/src/connector/client.rs +++ b/backend/src/connector/client.rs @@ -22,6 +22,8 @@ unused_variables )] +use hyper::body::to_bytes; + use super::{api::prelude::*, prelude::*}; /// Error type for failing to create a Client @@ -255,8 +257,7 @@ where body_handler: impl Fn(R) -> T + Send, ) -> Result { let body = response.into_body(); - let body = body - .into_raw() + let body = to_bytes(body) .await .change_context(APIError::ResponseError)?; let body = std::str::from_utf8(&body).change_context(APIError::ResponseError)?; @@ -268,6 +269,7 @@ where Ok(body_handler(body)) } } + impl Client where Cr: Credentials + Clone, @@ -286,10 +288,10 @@ where .change_context(APIError::RequestFailed) .attach_printable("No Response received")? .change_context(APIError::RequestFailed) + .attach_printable("Hyper error") } } -#[async_trait] impl Api for Client where Cr: Credentials + Clone, diff --git a/backend/src/connector/context.rs b/backend/src/connector/context.rs index d4b05c0c..c00ec005 100644 --- a/backend/src/connector/context.rs +++ b/backend/src/connector/context.rs @@ -86,38 +86,3 @@ where self.inner.call(req) } } - -/// Additional function for `hyper::Body` -pub trait BodyExt { - /// Raw body type - type Raw; - - /// Error if we can't gather up the raw body - type Error; - - /// Collect the body into a raw form - fn into_raw( - self, - ) -> futures::future::BoxFuture<'static, std::result::Result>; -} - -impl BodyExt for T -where - T: Stream> + Unpin + Send + 'static, -{ - type Raw = Vec; - type Error = E; - - fn into_raw( - mut self, - ) -> futures::future::BoxFuture<'static, std::result::Result> { - Box::pin(async { - let mut raw = Vec::new(); - while let (Some(chunk), rest) = self.into_future().await { - raw.extend_from_slice(&chunk?); - self = rest; - } - Ok(raw) - }) - } -} diff --git a/backend/src/connector/error.rs b/backend/src/connector/error.rs new file mode 100644 index 00000000..61b5dd35 --- /dev/null +++ b/backend/src/connector/error.rs @@ -0,0 +1,129 @@ +use super::prelude::*; + +impl From for StatusCode { + fn from(value: GetAlienResponse) -> Self { + match value { + GetAlienResponse::AlienNodeName(_) => StatusCode::OK, + } + } +} + +impl From for StatusCode { + fn from(value: GetAlienDirResponse) -> Self { + match value { + GetAlienDirResponse::Directory(_) => StatusCode::OK, + GetAlienDirResponse::PermissionDenied(_) => StatusCode::FORBIDDEN, + GetAlienDirResponse::NotAcceptableBackend(_) => StatusCode::NOT_ACCEPTABLE, + } + } +} + +impl From for StatusCode { + fn from(value: GetDisksResponse) -> Self { + match value { + GetDisksResponse::AJSONArrayWithDisksAndTheirStates(_) => StatusCode::OK, + GetDisksResponse::PermissionDenied(_) => StatusCode::FORBIDDEN, + } + } +} + +impl From for StatusCode { + fn from(value: GetMetricsResponse) -> Self { + match value { + GetMetricsResponse::Metrics(_) => StatusCode::OK, + } + } +} + +impl From for StatusCode { + fn from(value: GetNodesResponse) -> Self { + match value { + GetNodesResponse::AJSONArrayOfNodesInfoAndVdisksOnThem(_) => StatusCode::OK, + GetNodesResponse::PermissionDenied => StatusCode::FORBIDDEN, + } + } +} + +impl From for StatusCode { + fn from(value: GetPartitionResponse) -> Self { + match value { + GetPartitionResponse::AJSONWithPartitionInfo(_) => StatusCode::OK, + GetPartitionResponse::PermissionDenied(_) => StatusCode::FORBIDDEN, + GetPartitionResponse::NotFound(_) => StatusCode::NOT_FOUND, + } + } +} + +impl From for StatusCode { + fn from(value: GetRecordsResponse) -> Self { + match value { + GetRecordsResponse::RecordsCount(_) => StatusCode::OK, + GetRecordsResponse::PermissionDenied(_) => StatusCode::FORBIDDEN, + GetRecordsResponse::NotFound(_) => StatusCode::NOT_FOUND, + } + } +} + +impl From for StatusCode { + fn from(value: GetReplicasLocalDirsResponse) -> Self { + match value { + GetReplicasLocalDirsResponse::AJSONArrayWithDirs(_) => StatusCode::OK, + GetReplicasLocalDirsResponse::PermissionDenied(_) => StatusCode::FORBIDDEN, + GetReplicasLocalDirsResponse::NotFound(_) => StatusCode::NOT_FOUND, + } + } +} + +impl From for StatusCode { + fn from(value: GetStatusResponse) -> Self { + match value { + GetStatusResponse::AJSONWithNodeInfo(_) => StatusCode::OK, + } + } +} + +impl From for StatusCode { + fn from(value: GetVDiskResponse) -> Self { + match value { + GetVDiskResponse::AJSONWithVdiskInfo(_) => StatusCode::OK, + GetVDiskResponse::PermissionDenied(_) => StatusCode::FORBIDDEN, + GetVDiskResponse::NotFound(_) => StatusCode::NOT_FOUND, + } + } +} + +impl From for StatusCode { + fn from(value: GetVDisksResponse) -> Self { + match value { + GetVDisksResponse::AJSONArrayOfVdisksInfo(_) => StatusCode::OK, + GetVDisksResponse::PermissionDenied => StatusCode::FORBIDDEN, + } + } +} + +impl From for StatusCode { + fn from(value: GetVersionResponse) -> Self { + match value { + GetVersionResponse::VersionInfo(_) => StatusCode::OK, + } + } +} + +impl From for StatusCode { + fn from(value: GetConfigurationResponse) -> Self { + match value { + GetConfigurationResponse::ConfigurationObject(_) => StatusCode::OK, + GetConfigurationResponse::PermissionDenied => StatusCode::FORBIDDEN, + } + } +} + +pub trait AsApiError { + fn as_invalid_status(self) -> APIError; +} + +impl> AsApiError for T { + fn as_invalid_status(self) -> APIError { + APIError::InvalidStatusCode(self.into()) + } +} diff --git a/backend/src/connector/mod.rs b/backend/src/connector/mod.rs index ac072c41..114be2cb 100644 --- a/backend/src/connector/mod.rs +++ b/backend/src/connector/mod.rs @@ -1,6 +1,7 @@ mod prelude { + pub use super::api::prelude::*; pub use super::{ - context::{BodyExt, ContextWrapper, DropContextService, Has}, + context::{ContextWrapper, DropContextService, Has}, ClientError, Connector, }; pub use crate::{models::shared::XSpanIdString, prelude::*, services::auth::HttpClient}; @@ -8,8 +9,8 @@ mod prelude { headers::{authorization::Credentials, Authorization, HeaderMapExt}, http::{HeaderName, HeaderValue}, }; - pub use futures::{Stream, StreamExt}; - pub use hyper::{body::Bytes, service::Service, Response, Uri}; + pub use futures::StreamExt; + pub use hyper::{service::Service, Response, Uri}; pub use std::{ str::FromStr, sync::Arc, @@ -17,15 +18,18 @@ mod prelude { }; } -use api::{APIError, ApiNoContext, ContextWrapperExt}; +use api::{ApiNoContext, ContextWrapperExt}; use client::Client; use context::ClientContext; use prelude::*; +use self::error::AsApiError; + pub mod api; pub mod client; pub mod context; pub mod dto; +pub mod error; pub type ApiInterface = dyn ApiNoContext + Send + Sync; @@ -122,7 +126,6 @@ impl + Send + Sync + Clone> std::fmt::Debug } impl + Send + Sync> BobClient { - // impl BobClient { /// Creates new [`BobClient`] from [`BobConnectionData`] /// /// # Errors @@ -140,19 +143,22 @@ impl + Send + Sync> BobClient> = nodes @@ -212,7 +218,7 @@ impl + Send + Sync> BobClient + Send + Sync> BobClient Result { - if let Some(client) = self.cluster.get(name) { - match client - .get_nodes() - .await - .change_context(ClientError::Inaccessible)? - { - api::GetNodesResponse::AJSONArrayOfNodesInfoAndVdisksOnThem(_) => { - Ok(StatusCode::OK) - } - api::GetNodesResponse::PermissionDenied => { - Err(ClientError::PermissionDenied.into()) - } - } - } else { - Err(ClientError::NoClient.into()) - } - } - #[must_use] pub fn context(&self) -> &ClientContext { self.main.context() diff --git a/backend/src/lib.rs b/backend/src/lib.rs index a861a7db..d562f11d 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -1,4 +1,8 @@ -#![allow(clippy::multiple_crate_versions, clippy::module_name_repetitions)] +#![allow( + async_fn_in_trait, + clippy::multiple_crate_versions, + clippy::module_name_repetitions +)] #[cfg(all(feature = "swagger", debug_assertions))] use axum::{routing::get, Router}; diff --git a/backend/src/main.rs b/backend/src/main.rs index 79ff8ee1..97a908f7 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -44,7 +44,8 @@ async fn main() -> Result<(), AppError> { fn router(cors: CorsLayer) -> Router { let session_store = MemoryStore::default(); let session_service = ServiceBuilder::new() - .layer(HandleErrorLayer::new(|_: BoxError| async { + .layer(HandleErrorLayer::new(|err: BoxError| async move { + tracing::error!(err); StatusCode::BAD_REQUEST })) .layer( diff --git a/backend/src/models/shared.rs b/backend/src/models/shared.rs index 7f688050..1d5d6d27 100644 --- a/backend/src/models/shared.rs +++ b/backend/src/models/shared.rs @@ -81,7 +81,7 @@ pub struct BobConnectionData { } /// Optional auth credentials for a BOB cluster -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)] +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)] #[cfg_attr(all(feature = "swagger", debug_assertions), derive(ToSchema))] #[cfg_attr(all(feature = "swagger", debug_assertions), schema(example = json!({"login": "archeoss", "password": "12345"})))] pub struct Credentials { @@ -92,6 +92,15 @@ pub struct Credentials { pub password: String, } +#[allow(clippy::missing_fields_in_debug)] +impl std::fmt::Debug for Credentials { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Credentials") + .field("login", &self.login) + .finish() + } +} + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct RequestTimeout(Duration); @@ -129,12 +138,11 @@ impl XSpanIdString { x_span_id .and_then(|x| x.to_str().ok()) .map(|x| Self(x.to_string())) - .unwrap_or_default() + .unwrap_or_else(Self::gen) } -} -impl Default for XSpanIdString { - fn default() -> Self { + /// Generate Random `X-Span-ID` string. + pub fn gen() -> Self { Self(uuid::Uuid::new_v4().to_string()) } } diff --git a/backend/src/services/auth.rs b/backend/src/services/auth.rs index 762c1e4a..772ffb71 100644 --- a/backend/src/services/auth.rs +++ b/backend/src/services/auth.rs @@ -62,23 +62,29 @@ pub struct Credentials { (status = 404, description = "Can't reach specified hostname") ) ))] +#[tracing::instrument(ret, skip(auth), level = "info", fields(method = "POST"))] pub async fn login( mut auth: BobAuth, Extension(request_timeout): Extension, Json(bob): Json, ) -> AxumResult { - tracing::info!("post /login : {:?}", &bob); - let bob_client = BobClient::::try_new(bob.clone(), request_timeout) .await - .map_err(|err| match err.current_context() { - ClientError::InitClient => StatusCode::BAD_REQUEST, - ClientError::Inaccessible => StatusCode::NOT_FOUND, - ClientError::PermissionDenied => StatusCode::UNAUTHORIZED, - _ => StatusCode::INTERNAL_SERVER_ERROR, + .map_err(|err| { + tracing::error!("{err:?}"); + match err.current_context() { + ClientError::InitClient => StatusCode::BAD_REQUEST, + ClientError::Inaccessible => StatusCode::NOT_FOUND, + ClientError::PermissionDenied => StatusCode::UNAUTHORIZED, + _ => StatusCode::INTERNAL_SERVER_ERROR, + } })?; - let Ok(res) = bob_client.probe_main().await else { - return Err(StatusCode::UNAUTHORIZED.into()); + let res = match bob_client.probe_main().await { + Ok(res) => res, + Err(err) => { + tracing::error!("{err:?}"); + return Err(StatusCode::UNAUTHORIZED.into()); + } }; if res == StatusCode::OK { @@ -104,8 +110,6 @@ pub async fn login( StatusCode::UNAUTHORIZED })?; auth.client_store.insert(*bob_client.id(), bob_client); - tracing::info!("AUTHORIZATION SUCCESSFUL"); - tracing::info!("Logged in as {:?}", &auth.user()); } Ok(res) @@ -119,7 +123,7 @@ pub trait Store { /// /// # Errors /// - /// This function will return an error if a `Value` cpuldn't be loaded + /// This function will return an error if a `Value` couldn't be loaded async fn load(&self, user_id: &Id) -> Result, Self::Error>; /// Save `Value` into abstract store @@ -127,7 +131,7 @@ pub trait Store { /// /// # Errors /// - /// This function will return an error if a `Value` cpuldn't be loaded + /// This function will return an error if a `Value` couldn't be saved async fn save(&mut self, user_id: Id, value: Value) -> Result, Self::Error>; } @@ -200,8 +204,8 @@ where self.update_session().change_context(AuthError::LogoutError) } - fn user(&self) -> Option { - self.auth_data.user.clone() + const fn user(&self) -> Option<&User> { + self.auth_data.user.as_ref() } } @@ -224,6 +228,7 @@ where } } +// NOTE: async_trait is used in `FromRequestParts` declaration, so we still need to use it here #[async_trait] impl FromRequestParts for AuthStore where @@ -340,6 +345,7 @@ pub type BobAuth = AuthStore) -> impl IntoResponse { tracing::info!("post /logout : {:?}", &auth.auth_data); auth.logout().map_or_else( diff --git a/dockerfiles/alpine/Dockerfile b/dockerfiles/alpine/Dockerfile index 959d5d81..c9872d72 100644 --- a/dockerfiles/alpine/Dockerfile +++ b/dockerfiles/alpine/Dockerfile @@ -33,13 +33,13 @@ COPY cli/Cargo.toml cli/Cargo.toml COPY backend/Cargo.toml backend/Cargo.toml COPY utils/Cargo.toml utils/Cargo.toml COPY proc_macro/Cargo.toml proc_macro/Cargo.toml -COPY proc_macro/src/lib.rs proc_macro/src/lib.rs COPY frontend/Cargo.toml frontend/Cargo.toml COPY .cargo .cargo RUN echo "// if you see this, the build broke" > backend/src/lib.rs \ && echo "fn main() {println!(\"if you see this, the build broke\")}" > backend/src/main.rs \ && echo "fn main() {println!(\"if you see this, the build broke\")}" > frontend/build.rs \ && echo "// if you see this, the build broke" > cli/src/lib.rs \ + && echo "// if you see this, the build broke" > proc_macro/src/lib.rs \ && cargo build-backend --profile=$BUILD_PROFILE --target=$BUILD_TARGET COPY . ./