admin API refactoring (step 1) #939
8 changed files with 113 additions and 150 deletions
|
@ -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
|
||||||
|
|
|
@ -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
58
src/api/admin/macros.rs
Normal 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;
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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<'_>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -182,8 +182,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<'_>) {
|
||||||
|
|
|
@ -357,8 +357,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<'_>) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue