admin api: new router_v2 with unified path syntax

This commit is contained in:
Alex Auvolat
2025-01-28 15:12:03 +01:00
parent 831f2b0207
commit c99bfe69ea
16 changed files with 451 additions and 292 deletions
+20 -11
View File
@@ -11,6 +11,12 @@ use crate::admin::EndpointHandler;
use crate::helpers::is_default;
pub enum AdminApiRequest {
// Special endpoints of the Admin API
Options(OptionsRequest),
CheckDomain(CheckDomainRequest),
Health(HealthRequest),
Metrics(MetricsRequest),
// Cluster operations
GetClusterStatus(GetClusterStatusRequest),
GetClusterHealth(GetClusterHealthRequest),
@@ -90,6 +96,7 @@ impl EndpointHandler for AdminApiRequest {
async fn handle(self, garage: &Arc<Garage>) -> Result<AdminApiResponse, Error> {
Ok(match self {
Self::Options | Self::CheckDomain | Self::Health | Self::Metrics => unreachable!(),
// Cluster operations
Self::GetClusterStatus(req) => {
AdminApiResponse::GetClusterStatus(req.handle(garage).await?)
@@ -152,19 +159,19 @@ impl EndpointHandler for AdminApiRequest {
}
// **********************************************
// Metrics-related endpoints
// Special endpoints
// **********************************************
// TODO: do we want this here ??
pub struct OptionsRequest;
// ---- Metrics ----
pub struct MetricsRequest;
// ---- Health ----
pub struct CheckDomainRequest {
pub domain: String,
}
pub struct HealthRequest;
pub struct MetricsRequest;
// **********************************************
// Cluster operations
// **********************************************
@@ -404,7 +411,7 @@ pub struct ImportKeyResponse(pub GetKeyInfoResponse);
pub struct UpdateKeyRequest {
pub id: String,
pub params: UpdateKeyRequestParams,
pub body: UpdateKeyRequestBody,
}
#[derive(Serialize)]
@@ -412,7 +419,7 @@ pub struct UpdateKeyResponse(pub GetKeyInfoResponse);
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateKeyRequestParams {
pub struct UpdateKeyRequestBody {
// TODO: id (get parameter) goes here
pub name: Option<String>,
pub allow: Option<KeyPerm>,
@@ -527,7 +534,7 @@ pub struct CreateBucketLocalAlias {
pub struct UpdateBucketRequest {
pub id: String,
pub params: UpdateBucketRequestParams,
pub body: UpdateBucketRequestBody,
}
#[derive(Serialize)]
@@ -535,7 +542,7 @@ pub struct UpdateBucketResponse(pub GetBucketInfoResponse);
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateBucketRequestParams {
pub struct UpdateBucketRequestBody {
pub website_access: Option<UpdateBucketWebsiteAccess>,
pub quotas: Option<ApiBucketQuotas>,
}
@@ -563,6 +570,7 @@ pub struct DeleteBucketResponse;
// ---- BucketAllowKey ----
#[derive(Deserialize)]
pub struct BucketAllowKeyRequest(pub BucketKeyPermChangeRequest);
#[derive(Serialize)]
@@ -578,6 +586,7 @@ pub struct BucketKeyPermChangeRequest {
// ---- BucketDenyKey ----
#[derive(Deserialize)]
pub struct BucketDenyKeyRequest(pub BucketKeyPermChangeRequest);
#[derive(Serialize)]
+34 -262
View File
@@ -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<String>,
}
enum Endpoint {
Old(endpoint_v1::Endpoint),
New(String),
}
impl AdminApiServer {
pub fn new(
garage: Arc<Garage>,
@@ -67,130 +72,6 @@ impl AdminApiServer {
.await
}
fn handle_options(&self, _req: &Request<IncomingBody>) -> Result<Response<ResBody>, 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<IncomingBody>,
) -> Result<Response<ResBody>, Error> {
let query_params: HashMap<String, String> = 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<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) = 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<Response<ResBody>, 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<Response<ResBody>, Error> {
#[cfg(feature = "metrics")]
{
@@ -231,9 +112,13 @@ impl ApiHandler for AdminApiServer {
fn parse_endpoint(&self, req: &Request<IncomingBody>) -> Result<Endpoint, Error> {
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<IncomingBody>,
endpoint: Endpoint,
) -> Result<Response<ResBody>, 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<IncomingBody>,
) -> Result<AdminApiRequest, Error> {
match endpoint {
Endpoint::GetClusterStatus => {
Ok(AdminApiRequest::GetClusterStatus(GetClusterStatusRequest))
}
Endpoint::GetClusterHealth => {
Ok(AdminApiRequest::GetClusterHealth(GetClusterHealthRequest))
}
Endpoint::ConnectClusterNodes => {
let req = parse_json_body::<ConnectClusterNodesRequest, _, Error>(req).await?;
Ok(AdminApiRequest::ConnectClusterNodes(req))
}
// Layout
Endpoint::GetClusterLayout => {
Ok(AdminApiRequest::GetClusterLayout(GetClusterLayoutRequest))
}
Endpoint::UpdateClusterLayout => {
let updates = parse_json_body::<UpdateClusterLayoutRequest, _, Error>(req).await?;
Ok(AdminApiRequest::UpdateClusterLayout(updates))
}
Endpoint::ApplyClusterLayout => {
let param = parse_json_body::<ApplyClusterLayoutRequest, _, Error>(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::<CreateKeyRequest, _, Error>(req).await?;
Ok(AdminApiRequest::CreateKey(req))
}
Endpoint::ImportKey => {
let req = parse_json_body::<ImportKeyRequest, _, Error>(req).await?;
Ok(AdminApiRequest::ImportKey(req))
}
Endpoint::UpdateKey { id } => {
let params = parse_json_body::<UpdateKeyRequestParams, _, Error>(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::<CreateBucketRequest, _, Error>(req).await?;
Ok(AdminApiRequest::CreateBucket(req))
}
Endpoint::DeleteBucket { id } => {
Ok(AdminApiRequest::DeleteBucket(DeleteBucketRequest { id }))
}
Endpoint::UpdateBucket { id } => {
let params = parse_json_body::<UpdateBucketRequestParams, _, Error>(req).await?;
Ok(AdminApiRequest::UpdateBucket(UpdateBucketRequest {
id,
params,
}))
}
// Bucket-key permissions
Endpoint::BucketAllowKey => {
let req = parse_json_body::<BucketKeyPermChangeRequest, _, Error>(req).await?;
Ok(AdminApiRequest::BucketAllowKey(BucketAllowKeyRequest(req)))
}
Endpoint::BucketDenyKey => {
let req = parse_json_body::<BucketKeyPermChangeRequest, _, Error>(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<'_>) {}
+2 -2
View File
@@ -358,7 +358,7 @@ impl EndpointHandler for UpdateBucketRequest {
let state = bucket.state.as_option_mut().unwrap();
if let Some(wa) = self.params.website_access {
if let Some(wa) = self.body.website_access {
if wa.enabled {
state.website_config.update(Some(WebsiteConfig {
index_document: wa.index_document.ok_or_bad_request(
@@ -376,7 +376,7 @@ impl EndpointHandler for UpdateBucketRequest {
}
}
if let Some(q) = self.params.quotas {
if let Some(q) = self.body.quotas {
state.quotas.update(BucketQuotas {
max_size: q.max_size,
max_objects: q.max_objects,
+3 -3
View File
@@ -110,15 +110,15 @@ impl EndpointHandler for UpdateKeyRequest {
let key_state = key.state.as_option_mut().unwrap();
if let Some(new_name) = self.params.name {
if let Some(new_name) = self.body.name {
key_state.name.update(new_name);
}
if let Some(allow) = self.params.allow {
if let Some(allow) = self.body.allow {
if allow.create_bucket {
key_state.allow_create_bucket.update(true);
}
}
if let Some(deny) = self.params.deny {
if let Some(deny) = self.body.deny {
if deny.create_bucket {
key_state.allow_create_bucket.update(false);
}
+9 -2
View File
@@ -4,21 +4,28 @@ mod error;
pub mod api;
mod router_v0;
mod router_v1;
mod router_v2;
mod bucket;
mod cluster;
mod key;
mod special;
use std::sync::Arc;
use async_trait::async_trait;
use serde::Serialize;
use garage_model::garage::Garage;
pub enum Authorization {
None,
MetricsToken,
AdminToken,
}
#[async_trait]
pub trait EndpointHandler {
type Response: Serialize;
type Response;
async fn handle(self, garage: &Arc<Garage>) -> Result<Self::Response, error::Error>;
}
+1 -6
View File
@@ -4,14 +4,9 @@ use hyper::{Method, Request};
use crate::admin::error::*;
use crate::admin::router_v0;
use crate::admin::Authorization;
use crate::router_macros::*;
pub enum Authorization {
None,
MetricsToken,
AdminToken,
}
router_match! {@func
/// List of all Admin API endpoints.
+169
View File
@@ -0,0 +1,169 @@
use std::borrow::Cow;
use hyper::body::Incoming as IncomingBody;
use hyper::{Method, Request};
use paste::paste;
use crate::admin::api::*;
use crate::admin::error::*;
//use crate::admin::router_v1;
use crate::admin::Authorization;
use crate::helpers::*;
use crate::router_macros::*;
impl AdminApiRequest {
/// Determine which S3 endpoint a request is for using the request, and a bucket which was
/// possibly extracted from the Host header.
/// Returns Self plus bucket name, if endpoint is not Endpoint::ListBuckets
pub async fn from_request<T>(req: Request<IncomingBody>) -> Result<Self, Error> {
let uri = req.uri().clone();
let path = uri.path();
let query = uri.query();
let method = req.method().clone();
let mut query = QueryParameters::from_query(query.unwrap_or_default())?;
let res = router_match!(@gen_path_parser_v2 (&method, path, "/v2/", query, req) [
@special OPTIONS _ => Options (),
@special GET "/check" => CheckDomain (query::domain),
@special GET "/health" => Health (),
@special GET "/metrics" => Metrics (),
// Cluster endpoints
GET GetClusterStatus (),
GET GetClusterHealth (),
POST ConnectClusterNodes (body),
// Layout endpoints
GET GetClusterLayout (),
POST UpdateClusterLayout (body),
POST ApplyClusterLayout (body),
POST RevertClusterLayout (),
// API key endpoints
GET GetKeyInfo (query_opt::id, query_opt::search, parse_default(false)::show_secret_key),
POST UpdateKey (body_field, query::id),
POST CreateKey (body),
POST ImportKey (body),
DELETE DeleteKey (query::id),
GET ListKeys (),
// Bucket endpoints
GET GetBucketInfo (query_opt::id, query_opt::global_alias),
GET ListBuckets (),
POST CreateBucket (body),
DELETE DeleteBucket (query::id),
PUT UpdateBucket (body_field, query::id),
// Bucket-key permissions
POST BucketAllowKey (body),
POST BucketDenyKey (body),
// Bucket aliases
PUT GlobalAliasBucket (query::id, query::alias),
DELETE GlobalUnaliasBucket (query::id, query::alias),
PUT LocalAliasBucket (query::id, query::access_key_id, query::alias),
DELETE LocalUnaliasBucket (query::id, query::access_key_id, query::alias),
]);
if let Some(message) = query.nonempty_message() {
debug!("Unused query parameter: {}", message)
}
Ok(res)
}
/*
/// Some endpoints work exactly the same in their v1/ version as they did in their v0/ version.
/// For these endpoints, we can convert a v0/ call to its equivalent as if it was made using
/// its v1/ URL.
pub fn from_v0(v0_endpoint: router_v0::Endpoint) -> Result<Self, Error> {
match v0_endpoint {
// Cluster endpoints
router_v0::Endpoint::ConnectClusterNodes => Ok(Self::ConnectClusterNodes),
// - GetClusterStatus: response format changed
// - GetClusterHealth: response format changed
// Layout endpoints
router_v0::Endpoint::RevertClusterLayout => Ok(Self::RevertClusterLayout),
// - GetClusterLayout: response format changed
// - UpdateClusterLayout: query format changed
// - ApplyCusterLayout: response format changed
// Key endpoints
router_v0::Endpoint::ListKeys => Ok(Self::ListKeys),
router_v0::Endpoint::CreateKey => Ok(Self::CreateKey),
router_v0::Endpoint::GetKeyInfo { id, search } => Ok(Self::GetKeyInfo {
id,
search,
show_secret_key: Some("true".into()),
}),
router_v0::Endpoint::DeleteKey { id } => Ok(Self::DeleteKey { id }),
// - UpdateKey: response format changed (secret key no longer returned)
// Bucket endpoints
router_v0::Endpoint::GetBucketInfo { id, global_alias } => {
Ok(Self::GetBucketInfo { id, global_alias })
}
router_v0::Endpoint::ListBuckets => Ok(Self::ListBuckets),
router_v0::Endpoint::CreateBucket => Ok(Self::CreateBucket),
router_v0::Endpoint::DeleteBucket { id } => Ok(Self::DeleteBucket { id }),
router_v0::Endpoint::UpdateBucket { id } => Ok(Self::UpdateBucket { id }),
// Bucket-key permissions
router_v0::Endpoint::BucketAllowKey => Ok(Self::BucketAllowKey),
router_v0::Endpoint::BucketDenyKey => Ok(Self::BucketDenyKey),
// Bucket alias endpoints
router_v0::Endpoint::GlobalAliasBucket { id, alias } => {
Ok(Self::GlobalAliasBucket { id, alias })
}
router_v0::Endpoint::GlobalUnaliasBucket { id, alias } => {
Ok(Self::GlobalUnaliasBucket { id, alias })
}
router_v0::Endpoint::LocalAliasBucket {
id,
access_key_id,
alias,
} => Ok(Self::LocalAliasBucket {
id,
access_key_id,
alias,
}),
router_v0::Endpoint::LocalUnaliasBucket {
id,
access_key_id,
alias,
} => Ok(Self::LocalUnaliasBucket {
id,
access_key_id,
alias,
}),
// For endpoints that have different body content syntax, issue
// deprecation warning
_ => Err(Error::bad_request(format!(
"v0/ endpoint is no longer supported: {}",
v0_endpoint.name()
))),
}
}
*/
/// Get the kind of authorization which is required to perform the operation.
pub fn authorization_type(&self) -> Authorization {
match self {
Self::Health(_) => Authorization::None,
Self::CheckDomain(_) => Authorization::None,
Self::Metrics(_) => Authorization::MetricsToken,
_ => Authorization::AdminToken,
}
}
}
generateQueryParameters! {
keywords: [],
fields: [
"domain" => domain,
"format" => format,
"id" => id,
"search" => search,
"globalAlias" => global_alias,
"alias" => alias,
"accessKeyId" => access_key_id,
"showSecretKey" => show_secret_key
]
}
+129
View File
@@ -0,0 +1,129 @@
use std::sync::Arc;
use async_trait::async_trait;
use http::header::{ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ALLOW};
use hyper::{Response, StatusCode};
use garage_model::garage::Garage;
use garage_rpc::system::ClusterHealthStatus;
use crate::admin::api::{CheckDomainRequest, HealthRequest, OptionsRequest};
use crate::admin::api_server::ResBody;
use crate::admin::error::*;
use crate::admin::EndpointHandler;
use crate::helpers::*;
#[async_trait]
impl EndpointHandler for OptionsRequest {
type Response = Response<ResBody>;
async fn handle(self, _garage: &Arc<Garage>) -> Result<Response<ResBody>, 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_trait]
impl EndpointHandler for CheckDomainRequest {
type Response = Response<ResBody>;
async fn handle(self, garage: &Arc<Garage>) -> 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),
}
}
#[async_trait]
impl EndpointHandler for HealthRequest {
type Response = Response<ResBody>;
async fn handle(self, garage: &Arc<Garage>) -> 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))?)
}
}