admin api: new router_v2 with unified path syntax
Some checks failed
ci/woodpecker/push/debug Pipeline failed
ci/woodpecker/pr/debug Pipeline failed

This commit is contained in:
Alex 2025-01-28 15:12:03 +01:00
parent 4533b08f85
commit a67c1262ca
16 changed files with 4012 additions and 2835 deletions

1748
Cargo.lock generated

File diff suppressed because it is too large Load diff

4361
Cargo.nix

File diff suppressed because it is too large Load diff

View file

@ -62,6 +62,7 @@ mktemp = "0.5"
nix = { version = "0.29", default-features = false, features = ["fs"] }
nom = "7.1"
parse_duration = "2.1"
paste = "1.0"
pin-project = "1.0.12"
pnet_datalink = "0.34"
rand = "0.8"

View file

@ -38,6 +38,7 @@ idna.workspace = true
tracing.workspace = true
md-5.workspace = true
nom.workspace = true
paste.workspace = true
pin-project.workspace = true
sha1.workspace = true
sha2.workspace = true

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)]

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<'_>) {}

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,

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);
}

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>;
}

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
src/api/admin/router_v2.rs Normal file
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
]
}

128
src/api/admin/special.rs Normal file
View file

@ -0,0 +1,128 @@
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 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 /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))?)
}
}

View file

@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::convert::Infallible;
use std::fs::{self, Permissions};
use std::os::unix::fs::PermissionsExt;
@ -37,7 +38,7 @@ use garage_util::socket_address::UnixOrTCPSocketAddress;
use crate::helpers::{BoxBody, ErrorBody};
pub(crate) trait ApiEndpoint: Send + Sync + 'static {
fn name(&self) -> &'static str;
fn name(&self) -> Cow<'_, str>;
fn add_span_attributes(&self, span: SpanRef<'_>);
}

View file

@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::sync::Arc;
use async_trait::async_trait;
@ -179,8 +180,8 @@ impl ApiHandler for K2VApiServer {
}
impl ApiEndpoint for K2VApiEndpoint {
fn name(&self) -> &'static str {
self.endpoint.name()
fn name(&self) -> Cow<'_, str> {
Cow::borrowed(self.endpoint.name())
}
fn add_span_attributes(&self, span: SpanRef<'_>) {

View file

@ -44,6 +44,68 @@ macro_rules! router_match {
}
}
}};
(@gen_path_parser_v2 ($method:expr, $reqpath:expr, $pathprefix:literal, $query:expr, $req:expr)
[
$(@special $spec_meth:ident $spec_path:pat => $spec_api:ident $spec_params:tt,)*
$($meth:ident $api:ident $params:tt,)*
]) => {{
{
#[allow(unused_parens)]
match ($method, $reqpath) {
$(
(&Method::$spec_meth, $spec_path) => AdminApiRequest::$spec_api (
router_match!(@@gen_parse_request $spec_api, $spec_params, $query, $req)
),
)*
$(
(&Method::$meth, concat!($pathprefix, stringify!($api)))
=> AdminApiRequest::$api (
router_match!(@@gen_parse_request $api, $params, $query, $req)
),
)*
(m, p) => {
return Err(Error::bad_request(format!(
"Unknown API endpoint: {} {}",
m, p
)))
}
}
}
}};
(@@gen_parse_request $api:ident, (), $query: expr, $req:expr) => {{
paste!(
[< $api Request >]
)
}};
(@@gen_parse_request $api:ident, (body), $query: expr, $req:expr) => {{
paste!({
parse_json_body::< [<$api Request>], _, Error>($req).await?
})
}};
(@@gen_parse_request $api:ident, (body_field, $($conv:ident $(($conv_arg:expr))? :: $param:ident),*), $query: expr, $req:expr)
=>
{{
paste!({
let body = parse_json_body::< [<$api RequestBody>], _, Error>($req).await?;
[< $api Request >] {
body,
$(
$param: router_match!(@@parse_param $query, $conv $(($conv_arg))?, $param),
)+
}
})
}};
(@@gen_parse_request $api:ident, ($($conv:ident $(($conv_arg:expr))? :: $param:ident),*), $query: expr, $req:expr)
=>
{{
paste!({
[< $api Request >] {
$(
$param: router_match!(@@parse_param $query, $conv $(($conv_arg))?, $param),
)+
}
})
}};
(@gen_parser ($keyword:expr, $key:ident, $query:expr, $header:expr),
key: [$($kw_k:ident $(if $required_k:ident)? $(header $header_k:expr)? => $api_k:ident $(($($conv_k:ident :: $param_k:ident),*))?,)*],
no_key: [$($kw_nk:ident $(if $required_nk:ident)? $(if_header $header_nk:expr)? => $api_nk:ident $(($($conv_nk:ident :: $param_nk:ident),*))?,)*]) => {{
@ -102,6 +164,15 @@ macro_rules! router_match {
.parse()
.map_err(|_| Error::bad_request("Failed to parse query parameter"))?
}};
(@@parse_param $query:expr, parse_default($default:expr), $param:ident) => {{
// extract and parse mandatory query parameter
// both missing and un-parseable parameters are reported as errors
$query.$param.take().map(|x| x
.parse()
.map_err(|_| Error::bad_request("Failed to parse query parameter")))
.transpose()?
.unwrap_or($default)
}};
(@func
$(#[$doc:meta])*
pub enum Endpoint {

View file

@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::sync::Arc;
use async_trait::async_trait;
@ -355,8 +356,8 @@ impl ApiHandler for S3ApiServer {
}
impl ApiEndpoint for S3ApiEndpoint {
fn name(&self) -> &'static str {
self.endpoint.name()
fn name(&self) -> Cow<'_, str> {
Cow::borrowed(self.endpoint.name())
}
fn add_span_attributes(&self, span: SpanRef<'_>) {