From c99bfe69ea19497895d32669fd15c689b86035d8 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 28 Jan 2025 15:12:03 +0100 Subject: admin api: new router_v2 with unified path syntax --- src/api/admin/api_server.rs | 296 +++++--------------------------------------- 1 file changed, 34 insertions(+), 262 deletions(-) (limited to 'src/api/admin/api_server.rs') diff --git a/src/api/admin/api_server.rs b/src/api/admin/api_server.rs index c6b7661c..b235dafc 100644 --- a/src/api/admin/api_server.rs +++ b/src/api/admin/api_server.rs @@ -1,10 +1,10 @@ +use std::borrow::Cow; use std::collections::HashMap; use std::sync::Arc; use argon2::password_hash::PasswordHash; use async_trait::async_trait; -use http::header::{ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ALLOW}; use hyper::{body::Incoming as IncomingBody, Request, Response, StatusCode}; use tokio::sync::watch; @@ -25,7 +25,7 @@ use crate::generic_server::*; use crate::admin::api::*; use crate::admin::error::*; use crate::admin::router_v0; -use crate::admin::router_v1::{Authorization, Endpoint}; +use crate::admin::router_v1; use crate::admin::EndpointHandler; use crate::helpers::*; @@ -39,6 +39,11 @@ pub struct AdminApiServer { admin_token: Option, } +enum Endpoint { + Old(endpoint_v1::Endpoint), + New(String), +} + impl AdminApiServer { pub fn new( garage: Arc, @@ -67,130 +72,6 @@ impl AdminApiServer { .await } - fn handle_options(&self, _req: &Request) -> Result, Error> { - Ok(Response::builder() - .status(StatusCode::NO_CONTENT) - .header(ALLOW, "OPTIONS, GET, POST") - .header(ACCESS_CONTROL_ALLOW_METHODS, "OPTIONS, GET, POST") - .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") - .body(empty_body())?) - } - - async fn handle_check_domain( - &self, - req: Request, - ) -> Result, Error> { - let query_params: HashMap = req - .uri() - .query() - .map(|v| { - url::form_urlencoded::parse(v.as_bytes()) - .into_owned() - .collect() - }) - .unwrap_or_else(HashMap::new); - - let has_domain_key = query_params.contains_key("domain"); - - if !has_domain_key { - return Err(Error::bad_request("No domain query string found")); - } - - let domain = query_params - .get("domain") - .ok_or_internal_error("Could not parse domain query string")?; - - if self.check_domain(domain).await? { - Ok(Response::builder() - .status(StatusCode::OK) - .body(string_body(format!( - "Domain '{domain}' is managed by Garage" - )))?) - } else { - Err(Error::bad_request(format!( - "Domain '{domain}' is not managed by Garage" - ))) - } - } - - async fn check_domain(&self, domain: &str) -> Result { - // Resolve bucket from domain name, inferring if the website must be activated for the - // domain to be valid. - let (bucket_name, must_check_website) = if let Some(bname) = self - .garage - .config - .s3_api - .root_domain - .as_ref() - .and_then(|rd| host_to_bucket(domain, rd)) - { - (bname.to_string(), false) - } else if let Some(bname) = self - .garage - .config - .s3_web - .as_ref() - .and_then(|sw| host_to_bucket(domain, sw.root_domain.as_str())) - { - (bname.to_string(), true) - } else { - (domain.to_string(), true) - }; - - let bucket_id = match self - .garage - .bucket_helper() - .resolve_global_bucket_name(&bucket_name) - .await? - { - Some(bucket_id) => bucket_id, - None => return Ok(false), - }; - - if !must_check_website { - return Ok(true); - } - - let bucket = self - .garage - .bucket_helper() - .get_existing_bucket(bucket_id) - .await?; - - let bucket_state = bucket.state.as_option().unwrap(); - let bucket_website_config = bucket_state.website_config.get(); - - match bucket_website_config { - Some(_v) => Ok(true), - None => Ok(false), - } - } - - fn handle_health(&self) -> Result, Error> { - let health = self.garage.system.health(); - - let (status, status_str) = match health.status { - ClusterHealthStatus::Healthy => (StatusCode::OK, "Garage is fully operational"), - ClusterHealthStatus::Degraded => ( - StatusCode::OK, - "Garage is operational but some storage nodes are unavailable", - ), - ClusterHealthStatus::Unavailable => ( - StatusCode::SERVICE_UNAVAILABLE, - "Quorum is not available for some/all partitions, reads and writes will fail", - ), - }; - let status_str = format!( - "{}\nConsult the full health check API endpoint at /v1/health for more details\n", - status_str - ); - - Ok(Response::builder() - .status(status) - .header(http::header::CONTENT_TYPE, "text/plain") - .body(string_body(status_str))?) - } - fn handle_metrics(&self) -> Result, Error> { #[cfg(feature = "metrics")] { @@ -231,9 +112,13 @@ impl ApiHandler for AdminApiServer { fn parse_endpoint(&self, req: &Request) -> Result { if req.uri().path().starts_with("/v0/") { let endpoint_v0 = router_v0::Endpoint::from_request(req)?; - Endpoint::from_v0(endpoint_v0) + let endpoint_v1 = router_v1::Endpoint::from_v0(endpoint_v0); + Ok(Endpoint::Old(endpoint_v1)) + } else if req.uri().path().starts_with("/v1/") { + let endpoint_v1 = router_v1::Endpoint::from_request(req)?; + Ok(Endpoint::Old(endpoint_v1)) } else { - Endpoint::from_request(req) + Ok(Endpoint::New(req.uri().path().to_string())) } } @@ -242,8 +127,15 @@ impl ApiHandler for AdminApiServer { req: Request, endpoint: Endpoint, ) -> Result, Error> { + let request = match endpoint { + Endpoint::Old(endpoint_v1) => { + todo!() // TODO: convert from old semantics, if possible + } + Endpoint::New(_) => AdminApiRequest::from_request(req).await?, + }; + let required_auth_hash = - match endpoint.authorization_type() { + match request.authorization_type() { Authorization::None => None, Authorization::MetricsToken => self.metrics_token.as_deref(), Authorization::AdminToken => match self.admin_token.as_deref() { @@ -263,145 +155,25 @@ impl ApiHandler for AdminApiServer { } } - match endpoint { - Endpoint::Options => self.handle_options(&req), - Endpoint::CheckDomain => self.handle_check_domain(req).await, - Endpoint::Health => self.handle_health(), - Endpoint::Metrics => self.handle_metrics(), - e => { - async { - let body = parse_request_body(e, req).await?; - let res = body.handle(&self.garage).await?; - json_ok_response(&res) - } - .await + match request { + AdminApiRequest::Options(req) => req.handle(&self.garage).await, + AdminApiRequest::CheckDomain(req) => req.handle(&self.garage).await, + AdminApiRequest::Health(req) => req.handle(&self.garage).await, + AdminApiRequest::Metrics(req) => self.handle_metrics(), + req => { + let res = req.handle(&self.garage).await?; + json_ok_response(&res) } } } } -async fn parse_request_body( - endpoint: Endpoint, - req: Request, -) -> Result { - match endpoint { - Endpoint::GetClusterStatus => { - Ok(AdminApiRequest::GetClusterStatus(GetClusterStatusRequest)) - } - Endpoint::GetClusterHealth => { - Ok(AdminApiRequest::GetClusterHealth(GetClusterHealthRequest)) - } - Endpoint::ConnectClusterNodes => { - let req = parse_json_body::(req).await?; - Ok(AdminApiRequest::ConnectClusterNodes(req)) - } - // Layout - Endpoint::GetClusterLayout => { - Ok(AdminApiRequest::GetClusterLayout(GetClusterLayoutRequest)) - } - Endpoint::UpdateClusterLayout => { - let updates = parse_json_body::(req).await?; - Ok(AdminApiRequest::UpdateClusterLayout(updates)) - } - Endpoint::ApplyClusterLayout => { - let param = parse_json_body::(req).await?; - Ok(AdminApiRequest::ApplyClusterLayout(param)) - } - Endpoint::RevertClusterLayout => Ok(AdminApiRequest::RevertClusterLayout( - RevertClusterLayoutRequest, - )), - // Keys - Endpoint::ListKeys => Ok(AdminApiRequest::ListKeys(ListKeysRequest)), - Endpoint::GetKeyInfo { - id, - search, - show_secret_key, - } => { - let show_secret_key = show_secret_key.map(|x| x == "true").unwrap_or(false); - Ok(AdminApiRequest::GetKeyInfo(GetKeyInfoRequest { - id, - search, - show_secret_key, - })) - } - Endpoint::CreateKey => { - let req = parse_json_body::(req).await?; - Ok(AdminApiRequest::CreateKey(req)) - } - Endpoint::ImportKey => { - let req = parse_json_body::(req).await?; - Ok(AdminApiRequest::ImportKey(req)) - } - Endpoint::UpdateKey { id } => { - let params = parse_json_body::(req).await?; - Ok(AdminApiRequest::UpdateKey(UpdateKeyRequest { id, params })) - } - Endpoint::DeleteKey { id } => Ok(AdminApiRequest::DeleteKey(DeleteKeyRequest { id })), - // Buckets - Endpoint::ListBuckets => Ok(AdminApiRequest::ListBuckets(ListBucketsRequest)), - Endpoint::GetBucketInfo { id, global_alias } => { - Ok(AdminApiRequest::GetBucketInfo(GetBucketInfoRequest { - id, - global_alias, - })) - } - Endpoint::CreateBucket => { - let req = parse_json_body::(req).await?; - Ok(AdminApiRequest::CreateBucket(req)) - } - Endpoint::DeleteBucket { id } => { - Ok(AdminApiRequest::DeleteBucket(DeleteBucketRequest { id })) - } - Endpoint::UpdateBucket { id } => { - let params = parse_json_body::(req).await?; - Ok(AdminApiRequest::UpdateBucket(UpdateBucketRequest { - id, - params, - })) - } - // Bucket-key permissions - Endpoint::BucketAllowKey => { - let req = parse_json_body::(req).await?; - Ok(AdminApiRequest::BucketAllowKey(BucketAllowKeyRequest(req))) - } - Endpoint::BucketDenyKey => { - let req = parse_json_body::(req).await?; - Ok(AdminApiRequest::BucketDenyKey(BucketDenyKeyRequest(req))) - } - // Bucket aliasing - Endpoint::GlobalAliasBucket { id, alias } => Ok(AdminApiRequest::GlobalAliasBucket( - GlobalAliasBucketRequest { id, alias }, - )), - Endpoint::GlobalUnaliasBucket { id, alias } => Ok(AdminApiRequest::GlobalUnaliasBucket( - GlobalUnaliasBucketRequest { id, alias }, - )), - Endpoint::LocalAliasBucket { - id, - access_key_id, - alias, - } => Ok(AdminApiRequest::LocalAliasBucket(LocalAliasBucketRequest { - access_key_id, - id, - alias, - })), - Endpoint::LocalUnaliasBucket { - id, - access_key_id, - alias, - } => Ok(AdminApiRequest::LocalUnaliasBucket( - LocalUnaliasBucketRequest { - access_key_id, - id, - alias, - }, - )), - _ => unreachable!(), - } -} - impl ApiEndpoint for Endpoint { - fn name(&self) -> &'static str { - Endpoint::name(self) + fn name(&self) -> Cow<'_, str> { + match self { + Self::Old(endpoint_v1) => Cow::owned(format!("v1:{}", endpoint_v1.name)), + Self::New(path) => Cow::borrowed(&path), + } } fn add_span_attributes(&self, _span: SpanRef<'_>) {} -- cgit v1.2.3