aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Auvolat <lx@deuxfleurs.fr>2025-01-28 15:12:03 +0100
committerAlex Auvolat <lx@deuxfleurs.fr>2025-01-29 19:26:16 +0100
commitc99bfe69ea19497895d32669fd15c689b86035d8 (patch)
treefd70d3d92c45de20f28c078b902c083cee91a037
parent831f2b0207f128d67f061e6f7084337b1cbfefa4 (diff)
downloadgarage-c99bfe69ea19497895d32669fd15c689b86035d8.tar.gz
garage-c99bfe69ea19497895d32669fd15c689b86035d8.zip
admin api: new router_v2 with unified path syntax
-rw-r--r--Cargo.lock1
-rw-r--r--Cargo.nix3
-rw-r--r--Cargo.toml1
-rw-r--r--src/api/Cargo.toml1
-rw-r--r--src/api/admin/api.rs31
-rw-r--r--src/api/admin/api_server.rs296
-rw-r--r--src/api/admin/bucket.rs4
-rw-r--r--src/api/admin/key.rs6
-rw-r--r--src/api/admin/mod.rs11
-rw-r--r--src/api/admin/router_v1.rs7
-rw-r--r--src/api/admin/router_v2.rs169
-rw-r--r--src/api/admin/special.rs129
-rw-r--r--src/api/generic_server.rs3
-rw-r--r--src/api/k2v/api_server.rs5
-rw-r--r--src/api/router_macros.rs71
-rw-r--r--src/api/s3/api_server.rs5
16 files changed, 451 insertions, 292 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 0d3f70f0..ac39cbd2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1402,6 +1402,7 @@ dependencies = [
"nom",
"opentelemetry",
"opentelemetry-prometheus",
+ "paste",
"percent-encoding",
"pin-project",
"prometheus",
diff --git a/Cargo.nix b/Cargo.nix
index addc7629..fc6062f5 100644
--- a/Cargo.nix
+++ b/Cargo.nix
@@ -35,7 +35,7 @@ args@{
ignoreLockHash,
}:
let
- nixifiedLockHash = "d13a40f6a67a6a1075dbb5a948d7bfceea51958a0b5b6182ad56a9e39ab4dfd0";
+ nixifiedLockHash = "cc8c069ebe713e8225c166aa2bba5cc6e5016f007c6e7b7af36dd49452c859cc";
workspaceSrc = if args.workspaceSrc == null then ./. else args.workspaceSrc;
currentLockHash = builtins.hashFile "sha256" (workspaceSrc + /Cargo.lock);
lockHashIgnored = if ignoreLockHash
@@ -2042,6 +2042,7 @@ in
nom = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".nom."7.1.3" { inherit profileName; }).out;
opentelemetry = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry."0.17.0" { inherit profileName; }).out;
${ if rootFeatures' ? "garage/default" || rootFeatures' ? "garage/metrics" || rootFeatures' ? "garage_api/metrics" || rootFeatures' ? "garage_api/opentelemetry-prometheus" then "opentelemetry_prometheus" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".opentelemetry-prometheus."0.10.0" { inherit profileName; }).out;
+ paste = (buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".paste."1.0.14" { profileName = "__noProfile"; }).out;
percent_encoding = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".percent-encoding."2.3.1" { inherit profileName; }).out;
pin_project = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".pin-project."1.1.4" { inherit profileName; }).out;
${ if rootFeatures' ? "garage/default" || rootFeatures' ? "garage/metrics" || rootFeatures' ? "garage_api/metrics" || rootFeatures' ? "garage_api/prometheus" then "prometheus" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".prometheus."0.13.3" { inherit profileName; }).out;
diff --git a/Cargo.toml b/Cargo.toml
index 5ff0ec42..65e08f58 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"
diff --git a/src/api/Cargo.toml b/src/api/Cargo.toml
index 85b78a5b..1becbcdf 100644
--- a/src/api/Cargo.toml
+++ b/src/api/Cargo.toml
@@ -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
diff --git a/src/api/admin/api.rs b/src/api/admin/api.rs
index a5dbdfbe..b0ab058a 100644
--- a/src/api/admin/api.rs
+++ b/src/api/admin/api.rs
@@ -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 ??
-
-// ---- Metrics ----
+pub struct OptionsRequest;
-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)]
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<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<'_>) {}
diff --git a/src/api/admin/bucket.rs b/src/api/admin/bucket.rs
index d62bfa54..f9accba5 100644
--- a/src/api/admin/bucket.rs
+++ b/src/api/admin/bucket.rs
@@ -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,
diff --git a/src/api/admin/key.rs b/src/api/admin/key.rs
index 8161672f..5bec2202 100644
--- a/src/api/admin/key.rs
+++ b/src/api/admin/key.rs
@@ -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);
}
diff --git a/src/api/admin/mod.rs b/src/api/admin/mod.rs
index e64eca7e..f4c37298 100644
--- a/src/api/admin/mod.rs
+++ b/src/api/admin/mod.rs
@@ -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>;
}
diff --git a/src/api/admin/router_v1.rs b/src/api/admin/router_v1.rs
index cc5ff2ec..d69675cc 100644
--- a/src/api/admin/router_v1.rs
+++ b/src/api/admin/router_v1.rs
@@ -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.
diff --git a/src/api/admin/router_v2.rs b/src/api/admin/router_v2.rs
new file mode 100644
index 00000000..9d203500
--- /dev/null
+++ b/src/api/admin/router_v2.rs
@@ -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
+ ]
+}
diff --git a/src/api/admin/special.rs b/src/api/admin/special.rs
new file mode 100644
index 00000000..0239021a
--- /dev/null
+++ b/src/api/admin/special.rs
@@ -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))?)
+ }
+}
diff --git a/src/api/generic_server.rs b/src/api/generic_server.rs
index 283abdd4..ce2ff7b7 100644
--- a/src/api/generic_server.rs
+++ b/src/api/generic_server.rs
@@ -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<'_>);
}
diff --git a/src/api/k2v/api_server.rs b/src/api/k2v/api_server.rs
index de6e5f06..35931914 100644
--- a/src/api/k2v/api_server.rs
+++ b/src/api/k2v/api_server.rs
@@ -1,3 +1,4 @@
+use std::borrow::Cow;
use std::sync::Arc;
use async_trait::async_trait;
@@ -181,8 +182,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<'_>) {
diff --git a/src/api/router_macros.rs b/src/api/router_macros.rs
index 8f10a4f5..acbe097c 100644
--- a/src/api/router_macros.rs
+++ b/src/api/router_macros.rs
@@ -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 {
diff --git a/src/api/s3/api_server.rs b/src/api/s3/api_server.rs
index f9dafa10..3820ad8f 100644
--- a/src/api/s3/api_server.rs
+++ b/src/api/s3/api_server.rs
@@ -1,3 +1,4 @@
+use std::borrow::Cow;
use std::sync::Arc;
use async_trait::async_trait;
@@ -356,8 +357,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<'_>) {