admin api: refactor using macro
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:44:14 +01:00
parent f81c2333f1
commit 5eea6d3f0a
8 changed files with 113 additions and 150 deletions

View file

@ -2,161 +2,63 @@ use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use paste::paste;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use garage_model::garage::Garage; use garage_model::garage::Garage;
use crate::admin::error::Error; use crate::admin::error::Error;
use crate::admin::macros::*;
use crate::admin::EndpointHandler; use crate::admin::EndpointHandler;
use crate::helpers::is_default; use crate::helpers::is_default;
pub enum AdminApiRequest { // This generates the following:
// - An enum AdminApiRequest that contains a variant for all endpoints
// - An enum AdminApiResponse that contains a variant for all non-special endpoints
// - AdminApiRequest::name() that returns the name of the endpoint
// - impl EndpointHandler for AdminApiHandler, that uses the impl EndpointHandler
// of each request type below for non-special endpoints
admin_endpoints![
// Special endpoints of the Admin API // Special endpoints of the Admin API
Options(OptionsRequest), @special Options,
CheckDomain(CheckDomainRequest), @special CheckDomain,
Health(HealthRequest), @special Health,
Metrics(MetricsRequest), @special Metrics,
// Cluster operations // Cluster operations
GetClusterStatus(GetClusterStatusRequest), GetClusterStatus,
GetClusterHealth(GetClusterHealthRequest), GetClusterHealth,
ConnectClusterNodes(ConnectClusterNodesRequest), ConnectClusterNodes,
GetClusterLayout(GetClusterLayoutRequest), GetClusterLayout,
UpdateClusterLayout(UpdateClusterLayoutRequest), UpdateClusterLayout,
ApplyClusterLayout(ApplyClusterLayoutRequest), ApplyClusterLayout,
RevertClusterLayout(RevertClusterLayoutRequest), RevertClusterLayout,
// Access key operations // Access key operations
ListKeys(ListKeysRequest), ListKeys,
GetKeyInfo(GetKeyInfoRequest), GetKeyInfo,
CreateKey(CreateKeyRequest), CreateKey,
ImportKey(ImportKeyRequest), ImportKey,
UpdateKey(UpdateKeyRequest), UpdateKey,
DeleteKey(DeleteKeyRequest), DeleteKey,
// Bucket operations // Bucket operations
ListBuckets(ListBucketsRequest), ListBuckets,
GetBucketInfo(GetBucketInfoRequest), GetBucketInfo,
CreateBucket(CreateBucketRequest), CreateBucket,
UpdateBucket(UpdateBucketRequest), UpdateBucket,
DeleteBucket(DeleteBucketRequest), DeleteBucket,
// Operations on permissions for keys on buckets // Operations on permissions for keys on buckets
BucketAllowKey(BucketAllowKeyRequest), BucketAllowKey,
BucketDenyKey(BucketDenyKeyRequest), BucketDenyKey,
// Operations on bucket aliases // Operations on bucket aliases
GlobalAliasBucket(GlobalAliasBucketRequest), GlobalAliasBucket,
GlobalUnaliasBucket(GlobalUnaliasBucketRequest), GlobalUnaliasBucket,
LocalAliasBucket(LocalAliasBucketRequest), LocalAliasBucket,
LocalUnaliasBucket(LocalUnaliasBucketRequest), LocalUnaliasBucket,
} ];
#[derive(Serialize)]
#[serde(untagged)]
pub enum AdminApiResponse {
// Cluster operations
GetClusterStatus(GetClusterStatusResponse),
GetClusterHealth(GetClusterHealthResponse),
ConnectClusterNodes(ConnectClusterNodesResponse),
GetClusterLayout(GetClusterLayoutResponse),
UpdateClusterLayout(UpdateClusterLayoutResponse),
ApplyClusterLayout(ApplyClusterLayoutResponse),
RevertClusterLayout(RevertClusterLayoutResponse),
// Access key operations
ListKeys(ListKeysResponse),
GetKeyInfo(GetKeyInfoResponse),
CreateKey(CreateKeyResponse),
ImportKey(ImportKeyResponse),
UpdateKey(UpdateKeyResponse),
DeleteKey(DeleteKeyResponse),
// Bucket operations
ListBuckets(ListBucketsResponse),
GetBucketInfo(GetBucketInfoResponse),
CreateBucket(CreateBucketResponse),
UpdateBucket(UpdateBucketResponse),
DeleteBucket(DeleteBucketResponse),
// Operations on permissions for keys on buckets
BucketAllowKey(BucketAllowKeyResponse),
BucketDenyKey(BucketDenyKeyResponse),
// Operations on bucket aliases
GlobalAliasBucket(GlobalAliasBucketResponse),
GlobalUnaliasBucket(GlobalUnaliasBucketResponse),
LocalAliasBucket(LocalAliasBucketResponse),
LocalUnaliasBucket(LocalUnaliasBucketResponse),
}
#[async_trait]
impl EndpointHandler for AdminApiRequest {
type Response = AdminApiResponse;
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?)
}
Self::GetClusterHealth(req) => {
AdminApiResponse::GetClusterHealth(req.handle(garage).await?)
}
Self::ConnectClusterNodes(req) => {
AdminApiResponse::ConnectClusterNodes(req.handle(garage).await?)
}
Self::GetClusterLayout(req) => {
AdminApiResponse::GetClusterLayout(req.handle(garage).await?)
}
Self::UpdateClusterLayout(req) => {
AdminApiResponse::UpdateClusterLayout(req.handle(garage).await?)
}
Self::ApplyClusterLayout(req) => {
AdminApiResponse::ApplyClusterLayout(req.handle(garage).await?)
}
Self::RevertClusterLayout(req) => {
AdminApiResponse::RevertClusterLayout(req.handle(garage).await?)
}
// Access key operations
Self::ListKeys(req) => AdminApiResponse::ListKeys(req.handle(garage).await?),
Self::GetKeyInfo(req) => AdminApiResponse::GetKeyInfo(req.handle(garage).await?),
Self::CreateKey(req) => AdminApiResponse::CreateKey(req.handle(garage).await?),
Self::ImportKey(req) => AdminApiResponse::ImportKey(req.handle(garage).await?),
Self::UpdateKey(req) => AdminApiResponse::UpdateKey(req.handle(garage).await?),
Self::DeleteKey(req) => AdminApiResponse::DeleteKey(req.handle(garage).await?),
// Bucket operations
Self::ListBuckets(req) => AdminApiResponse::ListBuckets(req.handle(garage).await?),
Self::GetBucketInfo(req) => AdminApiResponse::GetBucketInfo(req.handle(garage).await?),
Self::CreateBucket(req) => AdminApiResponse::CreateBucket(req.handle(garage).await?),
Self::UpdateBucket(req) => AdminApiResponse::UpdateBucket(req.handle(garage).await?),
Self::DeleteBucket(req) => AdminApiResponse::DeleteBucket(req.handle(garage).await?),
// Operations on permissions for keys on buckets
Self::BucketAllowKey(req) => {
AdminApiResponse::BucketAllowKey(req.handle(garage).await?)
}
Self::BucketDenyKey(req) => AdminApiResponse::BucketDenyKey(req.handle(garage).await?),
// Operations on bucket aliases
Self::GlobalAliasBucket(req) => {
AdminApiResponse::GlobalAliasBucket(req.handle(garage).await?)
}
Self::GlobalUnaliasBucket(req) => {
AdminApiResponse::GlobalUnaliasBucket(req.handle(garage).await?)
}
Self::LocalAliasBucket(req) => {
AdminApiResponse::LocalAliasBucket(req.handle(garage).await?)
}
Self::LocalUnaliasBucket(req) => {
AdminApiResponse::LocalUnaliasBucket(req.handle(garage).await?)
}
})
}
}
// ********************************************** // **********************************************
// Special endpoints // Special endpoints

View file

@ -1,10 +1,10 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use argon2::password_hash::PasswordHash; use argon2::password_hash::PasswordHash;
use async_trait::async_trait; use async_trait::async_trait;
use http::header::AUTHORIZATION;
use hyper::{body::Incoming as IncomingBody, Request, Response, StatusCode}; use hyper::{body::Incoming as IncomingBody, Request, Response, StatusCode};
use tokio::sync::watch; use tokio::sync::watch;
@ -16,7 +16,6 @@ use opentelemetry_prometheus::PrometheusExporter;
use prometheus::{Encoder, TextEncoder}; use prometheus::{Encoder, TextEncoder};
use garage_model::garage::Garage; use garage_model::garage::Garage;
use garage_rpc::system::ClusterHealthStatus;
use garage_util::error::Error as GarageError; use garage_util::error::Error as GarageError;
use garage_util::socket_address::UnixOrTCPSocketAddress; use garage_util::socket_address::UnixOrTCPSocketAddress;
@ -26,6 +25,7 @@ use crate::admin::api::*;
use crate::admin::error::*; use crate::admin::error::*;
use crate::admin::router_v0; use crate::admin::router_v0;
use crate::admin::router_v1; use crate::admin::router_v1;
use crate::admin::Authorization;
use crate::admin::EndpointHandler; use crate::admin::EndpointHandler;
use crate::helpers::*; use crate::helpers::*;
@ -40,7 +40,7 @@ pub struct AdminApiServer {
} }
enum Endpoint { enum Endpoint {
Old(endpoint_v1::Endpoint), Old(router_v1::Endpoint),
New(String), New(String),
} }
@ -112,7 +112,7 @@ impl ApiHandler for AdminApiServer {
fn parse_endpoint(&self, req: &Request<IncomingBody>) -> Result<Endpoint, Error> { fn parse_endpoint(&self, req: &Request<IncomingBody>) -> Result<Endpoint, Error> {
if req.uri().path().starts_with("/v0/") { if req.uri().path().starts_with("/v0/") {
let endpoint_v0 = router_v0::Endpoint::from_request(req)?; let endpoint_v0 = router_v0::Endpoint::from_request(req)?;
let endpoint_v1 = router_v1::Endpoint::from_v0(endpoint_v0); let endpoint_v1 = router_v1::Endpoint::from_v0(endpoint_v0)?;
Ok(Endpoint::Old(endpoint_v1)) Ok(Endpoint::Old(endpoint_v1))
} else if req.uri().path().starts_with("/v1/") { } else if req.uri().path().starts_with("/v1/") {
let endpoint_v1 = router_v1::Endpoint::from_request(req)?; let endpoint_v1 = router_v1::Endpoint::from_request(req)?;
@ -127,6 +127,8 @@ impl ApiHandler for AdminApiServer {
req: Request<IncomingBody>, req: Request<IncomingBody>,
endpoint: Endpoint, endpoint: Endpoint,
) -> Result<Response<ResBody>, Error> { ) -> Result<Response<ResBody>, Error> {
let auth_header = req.headers().get(AUTHORIZATION).clone();
let request = match endpoint { let request = match endpoint {
Endpoint::Old(endpoint_v1) => { Endpoint::Old(endpoint_v1) => {
todo!() // TODO: convert from old semantics, if possible todo!() // TODO: convert from old semantics, if possible
@ -147,7 +149,7 @@ impl ApiHandler for AdminApiServer {
}; };
if let Some(password_hash) = required_auth_hash { if let Some(password_hash) = required_auth_hash {
match req.headers().get("Authorization") { match auth_header {
None => return Err(Error::forbidden("Authorization token must be provided")), None => return Err(Error::forbidden("Authorization token must be provided")),
Some(authorization) => { Some(authorization) => {
verify_bearer_token(&authorization, password_hash)?; verify_bearer_token(&authorization, password_hash)?;
@ -169,10 +171,10 @@ impl ApiHandler for AdminApiServer {
} }
impl ApiEndpoint for Endpoint { impl ApiEndpoint for Endpoint {
fn name(&self) -> Cow<'_, str> { fn name(&self) -> Cow<'static, str> {
match self { match self {
Self::Old(endpoint_v1) => Cow::owned(format!("v1:{}", endpoint_v1.name)), Self::Old(endpoint_v1) => Cow::Owned(format!("v1:{}", endpoint_v1.name())),
Self::New(path) => Cow::borrowed(&path), Self::New(path) => Cow::Owned(path.clone()),
} }
} }

58
src/api/admin/macros.rs Normal file
View file

@ -0,0 +1,58 @@
macro_rules! admin_endpoints {
[
$(@special $special_endpoint:ident,)*
$($endpoint:ident,)*
] => {
paste! {
pub enum AdminApiRequest {
$(
$special_endpoint( [<$special_endpoint Request>] ),
)*
$(
$endpoint( [<$endpoint Request>] ),
)*
}
#[derive(Serialize)]
#[serde(untagged)]
pub enum AdminApiResponse {
$(
$endpoint( [<$endpoint Response>] ),
)*
}
impl AdminApiRequest {
fn name(&self) -> &'static str {
match self {
$(
Self::$special_endpoint(_) => stringify!($special_endpoint),
)*
$(
Self::$endpoint(_) => stringify!($endpoint),
)*
}
}
}
#[async_trait]
impl EndpointHandler for AdminApiRequest {
type Response = AdminApiResponse;
async fn handle(self, garage: &Arc<Garage>) -> Result<AdminApiResponse, Error> {
Ok(match self {
$(
AdminApiRequest::$special_endpoint(_) => panic!(
concat!(stringify!($special_endpoint), " needs to go through a special handler")
),
)*
$(
AdminApiRequest::$endpoint(req) => AdminApiResponse::$endpoint(req.handle(garage).await?),
)*
})
}
}
}
};
}
pub(crate) use admin_endpoints;

View file

@ -1,5 +1,6 @@
pub mod api_server; pub mod api_server;
mod error; mod error;
mod macros;
pub mod api; pub mod api;
mod router_v0; mod router_v0;

View file

@ -15,7 +15,7 @@ impl AdminApiRequest {
/// Determine which S3 endpoint a request is for using the request, and a bucket which was /// Determine which S3 endpoint a request is for using the request, and a bucket which was
/// possibly extracted from the Host header. /// possibly extracted from the Host header.
/// Returns Self plus bucket name, if endpoint is not Endpoint::ListBuckets /// Returns Self plus bucket name, if endpoint is not Endpoint::ListBuckets
pub async fn from_request<T>(req: Request<IncomingBody>) -> Result<Self, Error> { pub async fn from_request(req: Request<IncomingBody>) -> Result<Self, Error> {
let uri = req.uri().clone(); let uri = req.uri().clone();
let path = uri.path(); let path = uri.path();
let query = uri.query(); let query = uri.query();

View file

@ -38,7 +38,7 @@ use garage_util::socket_address::UnixOrTCPSocketAddress;
use crate::helpers::{BoxBody, ErrorBody}; use crate::helpers::{BoxBody, ErrorBody};
pub(crate) trait ApiEndpoint: Send + Sync + 'static { pub(crate) trait ApiEndpoint: Send + Sync + 'static {
fn name(&self) -> Cow<'_, str>; fn name(&self) -> Cow<'static, str>;
fn add_span_attributes(&self, span: SpanRef<'_>); fn add_span_attributes(&self, span: SpanRef<'_>);
} }

View file

@ -180,8 +180,8 @@ impl ApiHandler for K2VApiServer {
} }
impl ApiEndpoint for K2VApiEndpoint { impl ApiEndpoint for K2VApiEndpoint {
fn name(&self) -> Cow<'_, str> { fn name(&self) -> Cow<'static, str> {
Cow::borrowed(self.endpoint.name()) Cow::Borrowed(self.endpoint.name())
} }
fn add_span_attributes(&self, span: SpanRef<'_>) { fn add_span_attributes(&self, span: SpanRef<'_>) {

View file

@ -356,8 +356,8 @@ impl ApiHandler for S3ApiServer {
} }
impl ApiEndpoint for S3ApiEndpoint { impl ApiEndpoint for S3ApiEndpoint {
fn name(&self) -> Cow<'_, str> { fn name(&self) -> Cow<'static, str> {
Cow::borrowed(self.endpoint.name()) Cow::Borrowed(self.endpoint.name())
} }
fn add_span_attributes(&self, span: SpanRef<'_>) { fn add_span_attributes(&self, span: SpanRef<'_>) {