admin api: new router_v2 with unified path syntax
This commit is contained in:
parent
4533b08f85
commit
f81c2333f1
16 changed files with 4013 additions and 2835 deletions
1748
Cargo.lock
generated
1748
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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<'_>) {}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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
169
src/api/admin/router_v2.rs
Normal 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
|
||||
]
|
||||
}
|
129
src/api/admin/special.rs
Normal file
129
src/api/admin/special.rs
Normal file
|
@ -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))?)
|
||||
}
|
||||
}
|
|
@ -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<'_>);
|
||||
}
|
||||
|
||||
|
|
|
@ -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<'_>) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<'_>) {
|
||||
|
|
Loading…
Add table
Reference in a new issue