diff options
Diffstat (limited to 'src/api/admin/special.rs')
-rw-r--r-- | src/api/admin/special.rs | 179 |
1 files changed, 179 insertions, 0 deletions
diff --git a/src/api/admin/special.rs b/src/api/admin/special.rs new file mode 100644 index 00000000..0ecf82bc --- /dev/null +++ b/src/api/admin/special.rs @@ -0,0 +1,179 @@ +use std::sync::Arc; + +use http::header::{ + ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ALLOW, +}; +use hyper::{Response, StatusCode}; + +#[cfg(feature = "metrics")] +use prometheus::{Encoder, TextEncoder}; + +use garage_model::garage::Garage; +use garage_rpc::system::ClusterHealthStatus; + +use garage_api_common::helpers::*; + +use crate::api::{CheckDomainRequest, HealthRequest, MetricsRequest, OptionsRequest}; +use crate::api_server::ResBody; +use crate::error::*; +use crate::{Admin, RequestHandler}; + +impl RequestHandler for OptionsRequest { + type Response = Response<ResBody>; + + async fn handle( + self, + _garage: &Arc<Garage>, + _admin: &Admin, + ) -> Result<Response<ResBody>, Error> { + Ok(Response::builder() + .status(StatusCode::OK) + .header(ALLOW, "OPTIONS,GET,POST") + .header(ACCESS_CONTROL_ALLOW_METHODS, "OPTIONS,GET,POST") + .header(ACCESS_CONTROL_ALLOW_HEADERS, "authorization,content-type") + .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(empty_body())?) + } +} + +impl RequestHandler for MetricsRequest { + type Response = Response<ResBody>; + + async fn handle( + self, + _garage: &Arc<Garage>, + admin: &Admin, + ) -> Result<Response<ResBody>, Error> { + #[cfg(feature = "metrics")] + { + use opentelemetry::trace::Tracer; + + let mut buffer = vec![]; + let encoder = TextEncoder::new(); + + let tracer = opentelemetry::global::tracer("garage"); + let metric_families = tracer.in_span("admin/gather_metrics", |_| { + admin.exporter.registry().gather() + }); + + encoder + .encode(&metric_families, &mut buffer) + .ok_or_internal_error("Could not serialize metrics")?; + + Ok(Response::builder() + .status(StatusCode::OK) + .header(http::header::CONTENT_TYPE, encoder.format_type()) + .body(bytes_body(buffer.into()))?) + } + #[cfg(not(feature = "metrics"))] + Err(Error::bad_request( + "Garage was built without the metrics feature".to_string(), + )) + } +} + +impl RequestHandler for HealthRequest { + type Response = Response<ResBody>; + + async fn handle( + self, + garage: &Arc<Garage>, + _admin: &Admin, + ) -> Result<Response<ResBody>, Error> { + let health = 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 /v2/GetClusterHealth for more details\n", + status_str + ); + + Ok(Response::builder() + .status(status) + .header(http::header::CONTENT_TYPE, "text/plain") + .body(string_body(status_str))?) + } +} + +impl RequestHandler for CheckDomainRequest { + type Response = Response<ResBody>; + + async fn handle( + self, + garage: &Arc<Garage>, + _admin: &Admin, + ) -> Result<Response<ResBody>, Error> { + if check_domain(garage, &self.domain).await? { + Ok(Response::builder() + .status(StatusCode::OK) + .body(string_body(format!( + "Domain '{}' is managed by Garage", + self.domain + )))?) + } else { + Err(Error::bad_request(format!( + "Domain '{}' is not managed by Garage", + self.domain + ))) + } + } +} + +async fn check_domain(garage: &Arc<Garage>, domain: &str) -> Result<bool, Error> { + // 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) = 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) = 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 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 = 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), + } +} |