admin API refactoring (step 1) #939

Merged
lx merged 19 commits from refactor-admin into next-v2 2025-01-29 20:42:57 +00:00
8 changed files with 113 additions and 150 deletions
Showing only changes of commit af1a530834 - Show all commits

View file

@ -2,161 +2,63 @@ use std::net::SocketAddr;
use std::sync::Arc;
use async_trait::async_trait;
use paste::paste;
use serde::{Deserialize, Serialize};
use garage_model::garage::Garage;
use crate::admin::error::Error;
use crate::admin::macros::*;
use crate::admin::EndpointHandler;
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
Options(OptionsRequest),
CheckDomain(CheckDomainRequest),
Health(HealthRequest),
Metrics(MetricsRequest),
@special Options,
@special CheckDomain,
@special Health,
@special Metrics,
// Cluster operations
GetClusterStatus(GetClusterStatusRequest),
GetClusterHealth(GetClusterHealthRequest),
ConnectClusterNodes(ConnectClusterNodesRequest),
GetClusterLayout(GetClusterLayoutRequest),
UpdateClusterLayout(UpdateClusterLayoutRequest),
ApplyClusterLayout(ApplyClusterLayoutRequest),
RevertClusterLayout(RevertClusterLayoutRequest),
GetClusterStatus,
GetClusterHealth,
ConnectClusterNodes,
GetClusterLayout,
UpdateClusterLayout,
ApplyClusterLayout,
RevertClusterLayout,
// Access key operations
ListKeys(ListKeysRequest),
GetKeyInfo(GetKeyInfoRequest),
CreateKey(CreateKeyRequest),
ImportKey(ImportKeyRequest),
UpdateKey(UpdateKeyRequest),
DeleteKey(DeleteKeyRequest),
ListKeys,
GetKeyInfo,
CreateKey,
ImportKey,
UpdateKey,
DeleteKey,
// Bucket operations
ListBuckets(ListBucketsRequest),
GetBucketInfo(GetBucketInfoRequest),
CreateBucket(CreateBucketRequest),
UpdateBucket(UpdateBucketRequest),
DeleteBucket(DeleteBucketRequest),
ListBuckets,
GetBucketInfo,
CreateBucket,
UpdateBucket,
DeleteBucket,
// Operations on permissions for keys on buckets
BucketAllowKey(BucketAllowKeyRequest),
BucketDenyKey(BucketDenyKeyRequest),
BucketAllowKey,
BucketDenyKey,
// Operations on bucket aliases
GlobalAliasBucket(GlobalAliasBucketRequest),
GlobalUnaliasBucket(GlobalUnaliasBucketRequest),
LocalAliasBucket(LocalAliasBucketRequest),
LocalUnaliasBucket(LocalUnaliasBucketRequest),
}
#[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?)
}
})
}
}
GlobalAliasBucket,
GlobalUnaliasBucket,
LocalAliasBucket,
LocalUnaliasBucket,
];
// **********************************************
// Special endpoints

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::AUTHORIZATION;
use hyper::{body::Incoming as IncomingBody, Request, Response, StatusCode};
use tokio::sync::watch;
@ -16,7 +16,6 @@ use opentelemetry_prometheus::PrometheusExporter;
use prometheus::{Encoder, TextEncoder};
use garage_model::garage::Garage;
use garage_rpc::system::ClusterHealthStatus;
use garage_util::error::Error as GarageError;
use garage_util::socket_address::UnixOrTCPSocketAddress;
@ -26,6 +25,7 @@ use crate::admin::api::*;
use crate::admin::error::*;
use crate::admin::router_v0;
use crate::admin::router_v1;
use crate::admin::Authorization;
use crate::admin::EndpointHandler;
use crate::helpers::*;
@ -40,7 +40,7 @@ pub struct AdminApiServer {
}
enum Endpoint {
Old(endpoint_v1::Endpoint),
Old(router_v1::Endpoint),
New(String),
}
@ -112,7 +112,7 @@ 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)?;
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))
} else if req.uri().path().starts_with("/v1/") {
let endpoint_v1 = router_v1::Endpoint::from_request(req)?;
@ -127,6 +127,8 @@ impl ApiHandler for AdminApiServer {
req: Request<IncomingBody>,
endpoint: Endpoint,
) -> Result<Response<ResBody>, Error> {
let auth_header = req.headers().get(AUTHORIZATION).clone();
let request = match endpoint {
Endpoint::Old(endpoint_v1) => {
todo!() // TODO: convert from old semantics, if possible
@ -147,7 +149,7 @@ impl ApiHandler for AdminApiServer {
};
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")),
Some(authorization) => {
verify_bearer_token(&authorization, password_hash)?;
@ -169,10 +171,10 @@ impl ApiHandler for AdminApiServer {
}
impl ApiEndpoint for Endpoint {
fn name(&self) -> Cow<'_, str> {
fn name(&self) -> Cow<'static, str> {
match self {
Self::Old(endpoint_v1) => Cow::owned(format!("v1:{}", endpoint_v1.name)),
Self::New(path) => Cow::borrowed(&path),
Self::Old(endpoint_v1) => Cow::Owned(format!("v1:{}", endpoint_v1.name())),
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;
mod error;
mod macros;
pub mod api;
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
/// 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> {
pub async fn from_request(req: Request<IncomingBody>) -> Result<Self, Error> {
let uri = req.uri().clone();
let path = uri.path();
let query = uri.query();

View file

@ -38,7 +38,7 @@ use garage_util::socket_address::UnixOrTCPSocketAddress;
use crate::helpers::{BoxBody, ErrorBody};
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<'_>);
}

View file

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

View file

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