From c1eb1610bab4d0d689dae9389f3fc10c0ab0efdc Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 27 Jan 2025 23:13:01 +0100 Subject: admin api: create structs for all requests/responess in src/api/admin/api.rs --- src/api/admin/api.rs | 486 ++++++++++++++++++++++++++++++++++++++++++++ src/api/admin/api_server.rs | 12 +- src/api/admin/bucket.rs | 210 ++++++------------- src/api/admin/cluster.rs | 374 +++++++++++++--------------------- src/api/admin/key.rs | 78 +------ src/api/admin/mod.rs | 16 ++ 6 files changed, 721 insertions(+), 455 deletions(-) create mode 100644 src/api/admin/api.rs (limited to 'src/api') diff --git a/src/api/admin/api.rs b/src/api/admin/api.rs new file mode 100644 index 00000000..a2dc95c2 --- /dev/null +++ b/src/api/admin/api.rs @@ -0,0 +1,486 @@ +use std::net::SocketAddr; + +use serde::{Deserialize, Serialize}; + +use crate::helpers::is_default; + +pub enum AdminApiRequest { + // Cluster operations + GetClusterStatus(GetClusterStatusRequest), + GetClusterHealth(GetClusterHealthRequest), + ConnectClusterNodes(ConnectClusterNodesRequest), + GetClusterLayout(GetClusterLayoutRequest), + UpdateClusterLayout(UpdateClusterLayoutRequest), + ApplyClusterLayout(ApplyClusterLayoutRequest), + RevertClusterLayout(RevertClusterLayoutRequest), +} + +pub enum AdminApiResponse { + // Cluster operations + GetClusterStatus(GetClusterStatusResponse), + GetClusterHealth(GetClusterHealthResponse), + ConnectClusterNodes(ConnectClusterNodesResponse), + GetClusterLayout(GetClusterLayoutResponse), + UpdateClusterLayout(UpdateClusterLayoutResponse), + ApplyClusterLayout(ApplyClusterLayoutResponse), + RevertClusterLayout(RevertClusterLayoutResponse), +} + +// ********************************************** +// Metrics-related endpoints +// ********************************************** + +// TODO: do we want this here ?? + +// ---- Metrics ---- + +pub struct MetricsRequest; + +// ---- Health ---- + +pub struct HealthRequest; + +// ********************************************** +// Cluster operations +// ********************************************** + +// ---- GetClusterStatus ---- + +pub struct GetClusterStatusRequest; + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetClusterStatusResponse { + pub node: String, + pub garage_version: &'static str, + pub garage_features: Option<&'static [&'static str]>, + pub rust_version: &'static str, + pub db_engine: String, + pub layout_version: u64, + pub nodes: Vec, +} + +#[derive(Serialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct NodeResp { + pub id: String, + pub role: Option, + pub addr: Option, + pub hostname: Option, + pub is_up: bool, + pub last_seen_secs_ago: Option, + pub draining: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub data_partition: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata_partition: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NodeRoleResp { + pub id: String, + pub zone: String, + pub capacity: Option, + pub tags: Vec, +} + +#[derive(Serialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct FreeSpaceResp { + pub available: u64, + pub total: u64, +} + +// ---- GetClusterHealth ---- + +pub struct GetClusterHealthRequest; + +#[derive(Debug, Clone, Copy, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetClusterHealthResponse { + pub status: &'static str, + pub known_nodes: usize, + pub connected_nodes: usize, + pub storage_nodes: usize, + pub storage_nodes_ok: usize, + pub partitions: usize, + pub partitions_quorum: usize, + pub partitions_all_ok: usize, +} + +// ---- ConnectClusterNodes ---- + +#[derive(Debug, Clone, Deserialize)] +pub struct ConnectClusterNodesRequest(pub Vec); + +#[derive(Serialize)] +pub struct ConnectClusterNodesResponse(pub Vec); + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectClusterNodeResponse { + pub success: bool, + pub error: Option, +} + +// ---- GetClusterLayout ---- + +pub struct GetClusterLayoutRequest; + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetClusterLayoutResponse { + pub version: u64, + pub roles: Vec, + pub staged_role_changes: Vec, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NodeRoleChange { + pub id: String, + #[serde(flatten)] + pub action: NodeRoleChangeEnum, +} + +#[derive(Serialize, Deserialize)] +#[serde(untagged)] +pub enum NodeRoleChangeEnum { + #[serde(rename_all = "camelCase")] + Remove { remove: bool }, + #[serde(rename_all = "camelCase")] + Update { + zone: String, + capacity: Option, + tags: Vec, + }, +} + +// ---- UpdateClusterLayout ---- + +#[derive(Deserialize)] +pub struct UpdateClusterLayoutRequest(pub Vec); + +#[derive(Serialize)] +pub struct UpdateClusterLayoutResponse(pub GetClusterLayoutResponse); + +// ---- ApplyClusterLayout ---- + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplyClusterLayoutRequest { + pub version: u64, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplyClusterLayoutResponse { + pub message: Vec, + pub layout: GetClusterLayoutResponse, +} + +// ---- RevertClusterLayout ---- + +pub struct RevertClusterLayoutRequest; + +#[derive(Serialize)] +pub struct RevertClusterLayoutResponse(pub GetClusterLayoutResponse); + +// ********************************************** +// Access key operations +// ********************************************** + +// ---- ListKeys ---- + +pub struct ListKeysRequest; + +#[derive(Serialize)] +pub struct ListKeysResponse(pub Vec); + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ListKeysResponseItem { + pub id: String, + pub name: String, +} + +// ---- GetKeyInfo ---- + +pub struct GetKeyInfoRequest { + pub id: Option, + pub search: Option, + pub show_secret_key: bool, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetKeyInfoResponse { + pub name: String, + pub access_key_id: String, + #[serde(skip_serializing_if = "is_default")] + pub secret_access_key: Option, + pub permissions: KeyPerm, + pub buckets: Vec, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct KeyPerm { + #[serde(default)] + pub create_bucket: bool, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct KeyInfoBucketResponse { + pub id: String, + pub global_aliases: Vec, + pub local_aliases: Vec, + pub permissions: ApiBucketKeyPerm, +} + +#[derive(Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct ApiBucketKeyPerm { + #[serde(default)] + pub read: bool, + #[serde(default)] + pub write: bool, + #[serde(default)] + pub owner: bool, +} + +// ---- CreateKey ---- + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateKeyRequest { + pub name: Option, +} + +#[derive(Serialize)] +pub struct CreateKeyResponse(pub GetKeyInfoResponse); + +// ---- ImportKey ---- + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ImportKeyRequest { + pub access_key_id: String, + pub secret_access_key: String, + pub name: Option, +} + +#[derive(Serialize)] +pub struct ImportKeyResponse(pub GetKeyInfoResponse); + +// ---- UpdateKey ---- + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UpdateKeyRequest { + // TODO: id (get parameter) goes here + pub name: Option, + pub allow: Option, + pub deny: Option, +} + +#[derive(Serialize)] +pub struct UpdateKeyResponse(pub GetKeyInfoResponse); + +// ---- DeleteKey ---- + +pub struct DeleteKeyRequest { + pub id: String, +} + +pub struct DeleteKeyResponse; + +// ********************************************** +// Bucket operations +// ********************************************** + +// ---- ListBuckets ---- + +pub struct ListBucketsRequest; + +pub struct ListBucketsResponse(pub Vec); + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ListBucketsResponseItem { + pub id: String, + pub global_aliases: Vec, + pub local_aliases: Vec, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BucketLocalAlias { + pub access_key_id: String, + pub alias: String, +} + +// ---- GetBucketInfo ---- + +pub struct GetBucketInfoRequest { + pub id: Option, + pub global_alias: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetBucketInfoResponse { + pub id: String, + pub global_aliases: Vec, + pub website_access: bool, + #[serde(default)] + pub website_config: Option, + pub keys: Vec, + pub objects: i64, + pub bytes: i64, + pub unfinished_uploads: i64, + pub unfinished_multipart_uploads: i64, + pub unfinished_multipart_upload_parts: i64, + pub unfinished_multipart_upload_bytes: i64, + pub quotas: ApiBucketQuotas, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetBucketInfoWebsiteResponse { + pub index_document: String, + pub error_document: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetBucketInfoKey { + pub access_key_id: String, + pub name: String, + pub permissions: ApiBucketKeyPerm, + pub bucket_local_aliases: Vec, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiBucketQuotas { + pub max_size: Option, + pub max_objects: Option, +} + +// ---- CreateBucket ---- + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateBucketRequest { + pub global_alias: Option, + pub local_alias: Option, +} + +#[derive(Serialize)] +pub struct CreateBucketResponse(GetBucketInfoResponse); + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateBucketLocalAlias { + pub access_key_id: String, + pub alias: String, + #[serde(default)] + pub allow: ApiBucketKeyPerm, +} + +// ---- UpdateBucket ---- + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UpdateBucketRequest { + pub website_access: Option, + pub quotas: Option, +} + +#[derive(Serialize)] +pub struct UpdateBucketResponse(GetBucketInfoResponse); + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UpdateBucketWebsiteAccess { + pub enabled: bool, + pub index_document: Option, + pub error_document: Option, +} + +// ---- DeleteBucket ---- + +pub struct DeleteBucketRequest { + pub id: String, +} + +pub struct DeleteBucketResponse; + +// ********************************************** +// Operations on permissions for keys on buckets +// ********************************************** + +// ---- BucketAllowKey ---- + +pub struct BucketAllowKeyRequest(pub BucketKeyPermChangeRequest); + +pub struct BucketAllowKeyResponse; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BucketKeyPermChangeRequest { + pub bucket_id: String, + pub access_key_id: String, + pub permissions: ApiBucketKeyPerm, +} + +// ---- BucketDenyKey ---- + +pub struct BucketDenyKeyRequest(pub BucketKeyPermChangeRequest); + +pub struct BucketDenyKeyResponse; + +// ********************************************** +// Operations on bucket aliases +// ********************************************** + +// ---- GlobalAliasBucket ---- + +pub struct GlobalAliasBucketRequest { + pub id: String, + pub alias: String, +} + +pub struct GlobalAliasBucketReponse; + +// ---- GlobalUnaliasBucket ---- + +pub struct GlobalUnaliasBucketRequest { + pub id: String, + pub alias: String, +} + +pub struct GlobalUnaliasBucketReponse; + +// ---- LocalAliasBucket ---- + +pub struct LocalAliasBucketRequest { + pub id: String, + pub access_key_id: String, + pub alias: String, +} + +pub struct LocalAliasBucketReponse; + +// ---- LocalUnaliasBucket ---- + +pub struct LocalUnaliasBucketRequest { + pub id: String, + pub access_key_id: String, + pub alias: String, +} + +pub struct LocalUnaliasBucketReponse; diff --git a/src/api/admin/api_server.rs b/src/api/admin/api_server.rs index 0e4565bb..9715292c 100644 --- a/src/api/admin/api_server.rs +++ b/src/api/admin/api_server.rs @@ -22,12 +22,14 @@ use garage_util::socket_address::UnixOrTCPSocketAddress; use crate::generic_server::*; +use crate::admin::api::*; use crate::admin::bucket::*; use crate::admin::cluster::*; use crate::admin::error::*; use crate::admin::key::*; use crate::admin::router_v0; use crate::admin::router_v1::{Authorization, Endpoint}; +use crate::admin::EndpointHandler; use crate::helpers::*; pub type ResBody = BoxBody; @@ -269,8 +271,14 @@ impl ApiHandler for AdminApiServer { Endpoint::CheckDomain => self.handle_check_domain(req).await, Endpoint::Health => self.handle_health(), Endpoint::Metrics => self.handle_metrics(), - Endpoint::GetClusterStatus => handle_get_cluster_status(&self.garage).await, - Endpoint::GetClusterHealth => handle_get_cluster_health(&self.garage).await, + Endpoint::GetClusterStatus => GetClusterStatusRequest + .handle(&self.garage) + .await + .and_then(|x| json_ok_response(&x)), + Endpoint::GetClusterHealth => GetClusterHealthRequest + .handle(&self.garage) + .await + .and_then(|x| json_ok_response(&x)), Endpoint::ConnectClusterNodes => handle_connect_cluster_nodes(&self.garage, req).await, // Layout Endpoint::GetClusterLayout => handle_get_cluster_layout(&self.garage).await, diff --git a/src/api/admin/bucket.rs b/src/api/admin/bucket.rs index ac3cba00..593848f0 100644 --- a/src/api/admin/bucket.rs +++ b/src/api/admin/bucket.rs @@ -2,7 +2,6 @@ use std::collections::HashMap; use std::sync::Arc; use hyper::{body::Incoming as IncomingBody, Request, Response, StatusCode}; -use serde::{Deserialize, Serialize}; use garage_util::crdt::*; use garage_util::data::*; @@ -17,9 +16,14 @@ use garage_model::permission::*; use garage_model::s3::mpu_table; use garage_model::s3::object_table::*; +use crate::admin::api::ApiBucketKeyPerm; +use crate::admin::api::{ + ApiBucketQuotas, BucketKeyPermChangeRequest, BucketLocalAlias, CreateBucketRequest, + GetBucketInfoKey, GetBucketInfoResponse, GetBucketInfoWebsiteResponse, ListBucketsResponseItem, + UpdateBucketRequest, +}; use crate::admin::api_server::ResBody; use crate::admin::error::*; -use crate::admin::key::ApiBucketKeyPerm; use crate::common_error::CommonError; use crate::helpers::*; @@ -39,7 +43,7 @@ pub async fn handle_list_buckets(garage: &Arc) -> Result) -> Result, - local_aliases: Vec, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct BucketLocalAlias { - access_key_id: String, - alias: String, -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct ApiBucketQuotas { - max_size: Option, - max_objects: Option, -} - pub async fn handle_get_bucket_info( garage: &Arc, id: Option, @@ -175,98 +157,63 @@ async fn bucket_info_results( let state = bucket.state.as_option().unwrap(); let quotas = state.quotas.get(); - let res = - GetBucketInfoResult { - id: hex::encode(bucket.id), - global_aliases: state - .aliases - .items() - .iter() - .filter(|(_, _, a)| *a) - .map(|(n, _, _)| n.to_string()) - .collect::>(), - website_access: state.website_config.get().is_some(), - website_config: state.website_config.get().clone().map(|wsc| { - GetBucketInfoWebsiteResult { - index_document: wsc.index_document, - error_document: wsc.error_document, + let res = GetBucketInfoResponse { + id: hex::encode(bucket.id), + global_aliases: state + .aliases + .items() + .iter() + .filter(|(_, _, a)| *a) + .map(|(n, _, _)| n.to_string()) + .collect::>(), + website_access: state.website_config.get().is_some(), + website_config: state.website_config.get().clone().map(|wsc| { + GetBucketInfoWebsiteResponse { + index_document: wsc.index_document, + error_document: wsc.error_document, + } + }), + keys: relevant_keys + .into_values() + .map(|key| { + let p = key.state.as_option().unwrap(); + GetBucketInfoKey { + access_key_id: key.key_id, + name: p.name.get().to_string(), + permissions: p + .authorized_buckets + .get(&bucket.id) + .map(|p| ApiBucketKeyPerm { + read: p.allow_read, + write: p.allow_write, + owner: p.allow_owner, + }) + .unwrap_or_default(), + bucket_local_aliases: p + .local_aliases + .items() + .iter() + .filter(|(_, _, b)| *b == Some(bucket.id)) + .map(|(n, _, _)| n.to_string()) + .collect::>(), } - }), - keys: relevant_keys - .into_values() - .map(|key| { - let p = key.state.as_option().unwrap(); - GetBucketInfoKey { - access_key_id: key.key_id, - name: p.name.get().to_string(), - permissions: p - .authorized_buckets - .get(&bucket.id) - .map(|p| ApiBucketKeyPerm { - read: p.allow_read, - write: p.allow_write, - owner: p.allow_owner, - }) - .unwrap_or_default(), - bucket_local_aliases: p - .local_aliases - .items() - .iter() - .filter(|(_, _, b)| *b == Some(bucket.id)) - .map(|(n, _, _)| n.to_string()) - .collect::>(), - } - }) - .collect::>(), - objects: *counters.get(OBJECTS).unwrap_or(&0), - bytes: *counters.get(BYTES).unwrap_or(&0), - unfinished_uploads: *counters.get(UNFINISHED_UPLOADS).unwrap_or(&0), - unfinished_multipart_uploads: *mpu_counters.get(mpu_table::UPLOADS).unwrap_or(&0), - unfinished_multipart_upload_parts: *mpu_counters.get(mpu_table::PARTS).unwrap_or(&0), - unfinished_multipart_upload_bytes: *mpu_counters.get(mpu_table::BYTES).unwrap_or(&0), - quotas: ApiBucketQuotas { - max_size: quotas.max_size, - max_objects: quotas.max_objects, - }, - }; + }) + .collect::>(), + objects: *counters.get(OBJECTS).unwrap_or(&0), + bytes: *counters.get(BYTES).unwrap_or(&0), + unfinished_uploads: *counters.get(UNFINISHED_UPLOADS).unwrap_or(&0), + unfinished_multipart_uploads: *mpu_counters.get(mpu_table::UPLOADS).unwrap_or(&0), + unfinished_multipart_upload_parts: *mpu_counters.get(mpu_table::PARTS).unwrap_or(&0), + unfinished_multipart_upload_bytes: *mpu_counters.get(mpu_table::BYTES).unwrap_or(&0), + quotas: ApiBucketQuotas { + max_size: quotas.max_size, + max_objects: quotas.max_objects, + }, + }; Ok(json_ok_response(&res)?) } -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct GetBucketInfoResult { - id: String, - global_aliases: Vec, - website_access: bool, - #[serde(default)] - website_config: Option, - keys: Vec, - objects: i64, - bytes: i64, - unfinished_uploads: i64, - unfinished_multipart_uploads: i64, - unfinished_multipart_upload_parts: i64, - unfinished_multipart_upload_bytes: i64, - quotas: ApiBucketQuotas, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct GetBucketInfoWebsiteResult { - index_document: String, - error_document: Option, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct GetBucketInfoKey { - access_key_id: String, - name: String, - permissions: ApiBucketKeyPerm, - bucket_local_aliases: Vec, -} - pub async fn handle_create_bucket( garage: &Arc, req: Request, @@ -336,22 +283,6 @@ pub async fn handle_create_bucket( bucket_info_results(garage, bucket.id).await } -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct CreateBucketRequest { - global_alias: Option, - local_alias: Option, -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct CreateBucketLocalAlias { - access_key_id: String, - alias: String, - #[serde(default)] - allow: ApiBucketKeyPerm, -} - pub async fn handle_delete_bucket( garage: &Arc, id: String, @@ -446,21 +377,6 @@ pub async fn handle_update_bucket( bucket_info_results(garage, bucket_id).await } -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct UpdateBucketRequest { - website_access: Option, - quotas: Option, -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct UpdateBucketWebsiteAccess { - enabled: bool, - index_document: Option, - error_document: Option, -} - // ---- BUCKET/KEY PERMISSIONS ---- pub async fn handle_bucket_change_key_perm( @@ -502,14 +418,6 @@ pub async fn handle_bucket_change_key_perm( bucket_info_results(garage, bucket.id).await } -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct BucketKeyPermChangeRequest { - bucket_id: String, - access_key_id: String, - permissions: ApiBucketKeyPerm, -} - // ---- BUCKET ALIASES ---- pub async fn handle_global_alias_bucket( diff --git a/src/api/admin/cluster.rs b/src/api/admin/cluster.rs index 357ac600..11753509 100644 --- a/src/api/admin/cluster.rs +++ b/src/api/admin/cluster.rs @@ -1,9 +1,8 @@ use std::collections::HashMap; -use std::net::SocketAddr; use std::sync::Arc; +use async_trait::async_trait; use hyper::{body::Incoming as IncomingBody, Request, Response}; -use serde::{Deserialize, Serialize}; use garage_util::crdt::*; use garage_util::data::*; @@ -12,153 +11,178 @@ use garage_rpc::layout; use garage_model::garage::Garage; +use crate::admin::api::{ + ApplyClusterLayoutRequest, ApplyClusterLayoutResponse, ConnectClusterNodeResponse, + ConnectClusterNodesRequest, ConnectClusterNodesResponse, FreeSpaceResp, + GetClusterHealthRequest, GetClusterHealthResponse, GetClusterLayoutResponse, + GetClusterStatusRequest, GetClusterStatusResponse, NodeResp, NodeRoleChange, + NodeRoleChangeEnum, NodeRoleResp, UpdateClusterLayoutRequest, +}; use crate::admin::api_server::ResBody; use crate::admin::error::*; +use crate::admin::EndpointHandler; use crate::helpers::{json_ok_response, parse_json_body}; -pub async fn handle_get_cluster_status(garage: &Arc) -> Result, Error> { - let layout = garage.system.cluster_layout(); - let mut nodes = garage - .system - .get_known_nodes() - .into_iter() - .map(|i| { - ( - i.id, - NodeResp { - id: hex::encode(i.id), - addr: i.addr, - hostname: i.status.hostname, - is_up: i.is_up, - last_seen_secs_ago: i.last_seen_secs_ago, - data_partition: i - .status - .data_disk_avail - .map(|(avail, total)| FreeSpaceResp { - available: avail, - total, +#[async_trait] +impl EndpointHandler for GetClusterStatusRequest { + type Response = GetClusterStatusResponse; + + async fn handle(self, garage: &Arc) -> Result { + let layout = garage.system.cluster_layout(); + let mut nodes = garage + .system + .get_known_nodes() + .into_iter() + .map(|i| { + ( + i.id, + NodeResp { + id: hex::encode(i.id), + addr: i.addr, + hostname: i.status.hostname, + is_up: i.is_up, + last_seen_secs_ago: i.last_seen_secs_ago, + data_partition: i.status.data_disk_avail.map(|(avail, total)| { + FreeSpaceResp { + available: avail, + total, + } }), - metadata_partition: i.status.meta_disk_avail.map(|(avail, total)| { - FreeSpaceResp { - available: avail, - total, - } - }), - ..Default::default() - }, - ) - }) - .collect::>(); - - for (id, _, role) in layout.current().roles.items().iter() { - if let layout::NodeRoleV(Some(r)) = role { - let role = NodeRoleResp { - id: hex::encode(id), - zone: r.zone.to_string(), - capacity: r.capacity, - tags: r.tags.clone(), - }; - match nodes.get_mut(id) { - None => { - nodes.insert( - *id, - NodeResp { - id: hex::encode(id), - role: Some(role), - ..Default::default() - }, - ); - } - Some(n) => { - n.role = Some(role); - } - } - } - } + metadata_partition: i.status.meta_disk_avail.map(|(avail, total)| { + FreeSpaceResp { + available: avail, + total, + } + }), + ..Default::default() + }, + ) + }) + .collect::>(); - for ver in layout.versions().iter().rev().skip(1) { - for (id, _, role) in ver.roles.items().iter() { + for (id, _, role) in layout.current().roles.items().iter() { if let layout::NodeRoleV(Some(r)) = role { - if r.capacity.is_some() { - if let Some(n) = nodes.get_mut(id) { - if n.role.is_none() { - n.draining = true; - } - } else { + let role = NodeRoleResp { + id: hex::encode(id), + zone: r.zone.to_string(), + capacity: r.capacity, + tags: r.tags.clone(), + }; + match nodes.get_mut(id) { + None => { nodes.insert( *id, NodeResp { id: hex::encode(id), - draining: true, + role: Some(role), ..Default::default() }, ); } + Some(n) => { + n.role = Some(role); + } } } } - } - let mut nodes = nodes.into_values().collect::>(); - nodes.sort_by(|x, y| x.id.cmp(&y.id)); - - let res = GetClusterStatusResponse { - node: hex::encode(garage.system.id), - garage_version: garage_util::version::garage_version(), - garage_features: garage_util::version::garage_features(), - rust_version: garage_util::version::rust_version(), - db_engine: garage.db.engine(), - layout_version: layout.current().version, - nodes, - }; + for ver in layout.versions().iter().rev().skip(1) { + for (id, _, role) in ver.roles.items().iter() { + if let layout::NodeRoleV(Some(r)) = role { + if r.capacity.is_some() { + if let Some(n) = nodes.get_mut(id) { + if n.role.is_none() { + n.draining = true; + } + } else { + nodes.insert( + *id, + NodeResp { + id: hex::encode(id), + draining: true, + ..Default::default() + }, + ); + } + } + } + } + } - Ok(json_ok_response(&res)?) + let mut nodes = nodes.into_values().collect::>(); + nodes.sort_by(|x, y| x.id.cmp(&y.id)); + + Ok(GetClusterStatusResponse { + node: hex::encode(garage.system.id), + garage_version: garage_util::version::garage_version(), + garage_features: garage_util::version::garage_features(), + rust_version: garage_util::version::rust_version(), + db_engine: garage.db.engine(), + layout_version: layout.current().version, + nodes, + }) + } } -pub async fn handle_get_cluster_health(garage: &Arc) -> Result, Error> { - use garage_rpc::system::ClusterHealthStatus; - let health = garage.system.health(); - let health = ClusterHealth { - status: match health.status { - ClusterHealthStatus::Healthy => "healthy", - ClusterHealthStatus::Degraded => "degraded", - ClusterHealthStatus::Unavailable => "unavailable", - }, - known_nodes: health.known_nodes, - connected_nodes: health.connected_nodes, - storage_nodes: health.storage_nodes, - storage_nodes_ok: health.storage_nodes_ok, - partitions: health.partitions, - partitions_quorum: health.partitions_quorum, - partitions_all_ok: health.partitions_all_ok, - }; - Ok(json_ok_response(&health)?) +#[async_trait] +impl EndpointHandler for GetClusterHealthRequest { + type Response = GetClusterHealthResponse; + + async fn handle(self, garage: &Arc) -> Result { + use garage_rpc::system::ClusterHealthStatus; + let health = garage.system.health(); + let health = GetClusterHealthResponse { + status: match health.status { + ClusterHealthStatus::Healthy => "healthy", + ClusterHealthStatus::Degraded => "degraded", + ClusterHealthStatus::Unavailable => "unavailable", + }, + known_nodes: health.known_nodes, + connected_nodes: health.connected_nodes, + storage_nodes: health.storage_nodes, + storage_nodes_ok: health.storage_nodes_ok, + partitions: health.partitions, + partitions_quorum: health.partitions_quorum, + partitions_all_ok: health.partitions_all_ok, + }; + Ok(health) + } } pub async fn handle_connect_cluster_nodes( garage: &Arc, req: Request, ) -> Result, Error> { - let req = parse_json_body::, _, Error>(req).await?; - - let res = futures::future::join_all(req.iter().map(|node| garage.system.connect(node))) - .await - .into_iter() - .map(|r| match r { - Ok(()) => ConnectClusterNodesResponse { - success: true, - error: None, - }, - Err(e) => ConnectClusterNodesResponse { - success: false, - error: Some(format!("{}", e)), - }, - }) - .collect::>(); + let req = parse_json_body::(req).await?; + + let res = req.handle(garage).await?; Ok(json_ok_response(&res)?) } +#[async_trait] +impl EndpointHandler for ConnectClusterNodesRequest { + type Response = ConnectClusterNodesResponse; + + async fn handle(self, garage: &Arc) -> Result { + let res = futures::future::join_all(self.0.iter().map(|node| garage.system.connect(node))) + .await + .into_iter() + .map(|r| match r { + Ok(()) => ConnectClusterNodeResponse { + success: true, + error: None, + }, + Err(e) => ConnectClusterNodeResponse { + success: false, + error: Some(format!("{}", e)), + }, + }) + .collect::>(); + Ok(ConnectClusterNodesResponse(res)) + } +} + pub async fn handle_get_cluster_layout(garage: &Arc) -> Result, Error> { let res = format_cluster_layout(garage.system.cluster_layout().inner()); @@ -212,85 +236,6 @@ fn format_cluster_layout(layout: &layout::LayoutHistory) -> GetClusterLayoutResp // ---- -#[derive(Debug, Clone, Copy, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ClusterHealth { - status: &'static str, - known_nodes: usize, - connected_nodes: usize, - storage_nodes: usize, - storage_nodes_ok: usize, - partitions: usize, - partitions_quorum: usize, - partitions_all_ok: usize, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct GetClusterStatusResponse { - node: String, - garage_version: &'static str, - garage_features: Option<&'static [&'static str]>, - rust_version: &'static str, - db_engine: String, - layout_version: u64, - nodes: Vec, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct ApplyClusterLayoutResponse { - message: Vec, - layout: GetClusterLayoutResponse, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct ConnectClusterNodesResponse { - success: bool, - error: Option, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct GetClusterLayoutResponse { - version: u64, - roles: Vec, - staged_role_changes: Vec, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct NodeRoleResp { - id: String, - zone: String, - capacity: Option, - tags: Vec, -} - -#[derive(Serialize, Default)] -#[serde(rename_all = "camelCase")] -struct FreeSpaceResp { - available: u64, - total: u64, -} - -#[derive(Serialize, Default)] -#[serde(rename_all = "camelCase")] -struct NodeResp { - id: String, - role: Option, - addr: Option, - hostname: Option, - is_up: bool, - last_seen_secs_ago: Option, - draining: bool, - #[serde(skip_serializing_if = "Option::is_none")] - data_partition: Option, - #[serde(skip_serializing_if = "Option::is_none")] - metadata_partition: Option, -} - // ---- update functions ---- pub async fn handle_update_cluster_layout( @@ -304,7 +249,7 @@ pub async fn handle_update_cluster_layout( let mut roles = layout.current().roles.clone(); roles.merge(&layout.staging.get().roles); - for change in updates { + for change in updates.0 { let node = hex::decode(&change.id).ok_or_bad_request("Invalid node identifier")?; let node = Uuid::try_from(&node).ok_or_bad_request("Invalid node identifier")?; @@ -343,7 +288,7 @@ pub async fn handle_apply_cluster_layout( garage: &Arc, req: Request, ) -> Result, Error> { - let param = parse_json_body::(req).await?; + let param = parse_json_body::(req).await?; let layout = garage.system.cluster_layout().inner().clone(); let (layout, msg) = layout.apply_staged_changes(Some(param.version))?; @@ -375,36 +320,3 @@ pub async fn handle_revert_cluster_layout( let res = format_cluster_layout(&layout); Ok(json_ok_response(&res)?) } - -// ---- - -type UpdateClusterLayoutRequest = Vec; - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct ApplyLayoutRequest { - version: u64, -} - -// ---- - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct NodeRoleChange { - id: String, - #[serde(flatten)] - action: NodeRoleChangeEnum, -} - -#[derive(Serialize, Deserialize)] -#[serde(untagged)] -enum NodeRoleChangeEnum { - #[serde(rename_all = "camelCase")] - Remove { remove: bool }, - #[serde(rename_all = "camelCase")] - Update { - zone: String, - capacity: Option, - tags: Vec, - }, -} diff --git a/src/api/admin/key.rs b/src/api/admin/key.rs index 291b6d54..96ce3518 100644 --- a/src/api/admin/key.rs +++ b/src/api/admin/key.rs @@ -2,13 +2,16 @@ use std::collections::HashMap; use std::sync::Arc; use hyper::{body::Incoming as IncomingBody, Request, Response, StatusCode}; -use serde::{Deserialize, Serialize}; use garage_table::*; use garage_model::garage::Garage; use garage_model::key_table::*; +use crate::admin::api::{ + ApiBucketKeyPerm, CreateKeyRequest, GetKeyInfoResponse, ImportKeyRequest, + KeyInfoBucketResponse, KeyPerm, ListKeysResponseItem, UpdateKeyRequest, +}; use crate::admin::api_server::ResBody; use crate::admin::error::*; use crate::helpers::*; @@ -25,7 +28,7 @@ pub async fn handle_list_keys(garage: &Arc) -> Result, ) .await? .iter() - .map(|k| ListKeyResultItem { + .map(|k| ListKeysResponseItem { id: k.key_id.to_string(), name: k.params().unwrap().name.get().clone(), }) @@ -34,13 +37,6 @@ pub async fn handle_list_keys(garage: &Arc) -> Result, Ok(json_ok_response(&res)?) } -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct ListKeyResultItem { - id: String, - name: String, -} - pub async fn handle_get_key_info( garage: &Arc, id: Option, @@ -73,12 +69,6 @@ pub async fn handle_create_key( key_info_results(garage, key, true).await } -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct CreateKeyRequest { - name: Option, -} - pub async fn handle_import_key( garage: &Arc, req: Request, @@ -101,14 +91,6 @@ pub async fn handle_import_key( key_info_results(garage, imported_key, false).await } -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct ImportKeyRequest { - access_key_id: String, - secret_access_key: String, - name: Option, -} - pub async fn handle_update_key( garage: &Arc, id: String, @@ -139,14 +121,6 @@ pub async fn handle_update_key( key_info_results(garage, key, false).await } -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct UpdateKeyRequest { - name: Option, - allow: Option, - deny: Option, -} - pub async fn handle_delete_key( garage: &Arc, id: String, @@ -192,7 +166,7 @@ async fn key_info_results( } } - let res = GetKeyInfoResult { + let res = GetKeyInfoResponse { name: key_state.name.get().clone(), access_key_id: key.key_id.clone(), secret_access_key: if show_secret { @@ -207,7 +181,7 @@ async fn key_info_results( .into_values() .map(|bucket| { let state = bucket.state.as_option().unwrap(); - KeyInfoBucketResult { + KeyInfoBucketResponse { id: hex::encode(bucket.id), global_aliases: state .aliases @@ -239,41 +213,3 @@ async fn key_info_results( Ok(json_ok_response(&res)?) } - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct GetKeyInfoResult { - name: String, - access_key_id: String, - #[serde(skip_serializing_if = "is_default")] - secret_access_key: Option, - permissions: KeyPerm, - buckets: Vec, -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct KeyPerm { - #[serde(default)] - create_bucket: bool, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct KeyInfoBucketResult { - id: String, - global_aliases: Vec, - local_aliases: Vec, - permissions: ApiBucketKeyPerm, -} - -#[derive(Serialize, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -pub(crate) struct ApiBucketKeyPerm { - #[serde(default)] - pub(crate) read: bool, - #[serde(default)] - pub(crate) write: bool, - #[serde(default)] - pub(crate) owner: bool, -} diff --git a/src/api/admin/mod.rs b/src/api/admin/mod.rs index 43a8c59c..e64eca7e 100644 --- a/src/api/admin/mod.rs +++ b/src/api/admin/mod.rs @@ -1,8 +1,24 @@ pub mod api_server; mod error; + +pub mod api; mod router_v0; mod router_v1; mod bucket; mod cluster; mod key; + +use std::sync::Arc; + +use async_trait::async_trait; +use serde::Serialize; + +use garage_model::garage::Garage; + +#[async_trait] +pub trait EndpointHandler { + type Response: Serialize; + + async fn handle(self, garage: &Arc) -> Result; +} -- cgit v1.2.3 From 831f2b0207f128d67f061e6f7084337b1cbfefa4 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 28 Jan 2025 00:22:14 +0100 Subject: admin api: make all handlers impls of a single trait --- src/api/admin/api.rs | 174 +++++++++++++-- src/api/admin/api_server.rs | 176 ++++++++++----- src/api/admin/bucket.rs | 522 ++++++++++++++++++++++++-------------------- src/api/admin/cluster.rs | 182 ++++++++------- src/api/admin/key.rs | 209 +++++++++--------- 5 files changed, 761 insertions(+), 502 deletions(-) (limited to 'src/api') diff --git a/src/api/admin/api.rs b/src/api/admin/api.rs index a2dc95c2..a5dbdfbe 100644 --- a/src/api/admin/api.rs +++ b/src/api/admin/api.rs @@ -1,7 +1,13 @@ use std::net::SocketAddr; +use std::sync::Arc; +use async_trait::async_trait; use serde::{Deserialize, Serialize}; +use garage_model::garage::Garage; + +use crate::admin::error::Error; +use crate::admin::EndpointHandler; use crate::helpers::is_default; pub enum AdminApiRequest { @@ -13,8 +19,35 @@ pub enum AdminApiRequest { UpdateClusterLayout(UpdateClusterLayoutRequest), ApplyClusterLayout(ApplyClusterLayoutRequest), RevertClusterLayout(RevertClusterLayoutRequest), + + // Access key operations + ListKeys(ListKeysRequest), + GetKeyInfo(GetKeyInfoRequest), + CreateKey(CreateKeyRequest), + ImportKey(ImportKeyRequest), + UpdateKey(UpdateKeyRequest), + DeleteKey(DeleteKeyRequest), + + // Bucket operations + ListBuckets(ListBucketsRequest), + GetBucketInfo(GetBucketInfoRequest), + CreateBucket(CreateBucketRequest), + UpdateBucket(UpdateBucketRequest), + DeleteBucket(DeleteBucketRequest), + + // Operations on permissions for keys on buckets + BucketAllowKey(BucketAllowKeyRequest), + BucketDenyKey(BucketDenyKeyRequest), + + // Operations on bucket aliases + GlobalAliasBucket(GlobalAliasBucketRequest), + GlobalUnaliasBucket(GlobalUnaliasBucketRequest), + LocalAliasBucket(LocalAliasBucketRequest), + LocalUnaliasBucket(LocalUnaliasBucketRequest), } +#[derive(Serialize)] +#[serde(untagged)] pub enum AdminApiResponse { // Cluster operations GetClusterStatus(GetClusterStatusResponse), @@ -24,6 +57,98 @@ pub enum AdminApiResponse { 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) -> Result { + Ok(match self { + // 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?) + } + }) + } } // ********************************************** @@ -277,24 +402,30 @@ pub struct ImportKeyResponse(pub GetKeyInfoResponse); // ---- UpdateKey ---- +pub struct UpdateKeyRequest { + pub id: String, + pub params: UpdateKeyRequestParams, +} + +#[derive(Serialize)] +pub struct UpdateKeyResponse(pub GetKeyInfoResponse); + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] -pub struct UpdateKeyRequest { +pub struct UpdateKeyRequestParams { // TODO: id (get parameter) goes here pub name: Option, pub allow: Option, pub deny: Option, } -#[derive(Serialize)] -pub struct UpdateKeyResponse(pub GetKeyInfoResponse); - // ---- DeleteKey ---- pub struct DeleteKeyRequest { pub id: String, } +#[derive(Serialize)] pub struct DeleteKeyResponse; // ********************************************** @@ -305,6 +436,7 @@ pub struct DeleteKeyResponse; pub struct ListBucketsRequest; +#[derive(Serialize)] pub struct ListBucketsResponse(pub Vec); #[derive(Serialize)] @@ -380,7 +512,7 @@ pub struct CreateBucketRequest { } #[derive(Serialize)] -pub struct CreateBucketResponse(GetBucketInfoResponse); +pub struct CreateBucketResponse(pub GetBucketInfoResponse); #[derive(Deserialize)] #[serde(rename_all = "camelCase")] @@ -393,16 +525,21 @@ pub struct CreateBucketLocalAlias { // ---- UpdateBucket ---- +pub struct UpdateBucketRequest { + pub id: String, + pub params: UpdateBucketRequestParams, +} + +#[derive(Serialize)] +pub struct UpdateBucketResponse(pub GetBucketInfoResponse); + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] -pub struct UpdateBucketRequest { +pub struct UpdateBucketRequestParams { pub website_access: Option, pub quotas: Option, } -#[derive(Serialize)] -pub struct UpdateBucketResponse(GetBucketInfoResponse); - #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct UpdateBucketWebsiteAccess { @@ -417,6 +554,7 @@ pub struct DeleteBucketRequest { pub id: String, } +#[derive(Serialize)] pub struct DeleteBucketResponse; // ********************************************** @@ -427,7 +565,8 @@ pub struct DeleteBucketResponse; pub struct BucketAllowKeyRequest(pub BucketKeyPermChangeRequest); -pub struct BucketAllowKeyResponse; +#[derive(Serialize)] +pub struct BucketAllowKeyResponse(pub GetBucketInfoResponse); #[derive(Deserialize)] #[serde(rename_all = "camelCase")] @@ -441,7 +580,8 @@ pub struct BucketKeyPermChangeRequest { pub struct BucketDenyKeyRequest(pub BucketKeyPermChangeRequest); -pub struct BucketDenyKeyResponse; +#[derive(Serialize)] +pub struct BucketDenyKeyResponse(pub GetBucketInfoResponse); // ********************************************** // Operations on bucket aliases @@ -454,7 +594,8 @@ pub struct GlobalAliasBucketRequest { pub alias: String, } -pub struct GlobalAliasBucketReponse; +#[derive(Serialize)] +pub struct GlobalAliasBucketResponse(pub GetBucketInfoResponse); // ---- GlobalUnaliasBucket ---- @@ -463,7 +604,8 @@ pub struct GlobalUnaliasBucketRequest { pub alias: String, } -pub struct GlobalUnaliasBucketReponse; +#[derive(Serialize)] +pub struct GlobalUnaliasBucketResponse(pub GetBucketInfoResponse); // ---- LocalAliasBucket ---- @@ -473,7 +615,8 @@ pub struct LocalAliasBucketRequest { pub alias: String, } -pub struct LocalAliasBucketReponse; +#[derive(Serialize)] +pub struct LocalAliasBucketResponse(pub GetBucketInfoResponse); // ---- LocalUnaliasBucket ---- @@ -483,4 +626,5 @@ pub struct LocalUnaliasBucketRequest { pub alias: String, } -pub struct LocalUnaliasBucketReponse; +#[derive(Serialize)] +pub struct LocalUnaliasBucketResponse(pub GetBucketInfoResponse); diff --git a/src/api/admin/api_server.rs b/src/api/admin/api_server.rs index 9715292c..c6b7661c 100644 --- a/src/api/admin/api_server.rs +++ b/src/api/admin/api_server.rs @@ -23,10 +23,7 @@ use garage_util::socket_address::UnixOrTCPSocketAddress; use crate::generic_server::*; use crate::admin::api::*; -use crate::admin::bucket::*; -use crate::admin::cluster::*; use crate::admin::error::*; -use crate::admin::key::*; use crate::admin::router_v0; use crate::admin::router_v1::{Authorization, Endpoint}; use crate::admin::EndpointHandler; @@ -271,67 +268,134 @@ impl ApiHandler for AdminApiServer { Endpoint::CheckDomain => self.handle_check_domain(req).await, Endpoint::Health => self.handle_health(), Endpoint::Metrics => self.handle_metrics(), - Endpoint::GetClusterStatus => GetClusterStatusRequest - .handle(&self.garage) - .await - .and_then(|x| json_ok_response(&x)), - Endpoint::GetClusterHealth => GetClusterHealthRequest - .handle(&self.garage) + e => { + async { + let body = parse_request_body(e, req).await?; + let res = body.handle(&self.garage).await?; + json_ok_response(&res) + } .await - .and_then(|x| json_ok_response(&x)), - Endpoint::ConnectClusterNodes => handle_connect_cluster_nodes(&self.garage, req).await, - // Layout - Endpoint::GetClusterLayout => handle_get_cluster_layout(&self.garage).await, - Endpoint::UpdateClusterLayout => handle_update_cluster_layout(&self.garage, req).await, - Endpoint::ApplyClusterLayout => handle_apply_cluster_layout(&self.garage, req).await, - Endpoint::RevertClusterLayout => handle_revert_cluster_layout(&self.garage).await, - // Keys - Endpoint::ListKeys => handle_list_keys(&self.garage).await, - Endpoint::GetKeyInfo { + } + } + } +} + +async fn parse_request_body( + endpoint: Endpoint, + req: Request, +) -> Result { + match endpoint { + Endpoint::GetClusterStatus => { + Ok(AdminApiRequest::GetClusterStatus(GetClusterStatusRequest)) + } + Endpoint::GetClusterHealth => { + Ok(AdminApiRequest::GetClusterHealth(GetClusterHealthRequest)) + } + Endpoint::ConnectClusterNodes => { + let req = parse_json_body::(req).await?; + Ok(AdminApiRequest::ConnectClusterNodes(req)) + } + // Layout + Endpoint::GetClusterLayout => { + Ok(AdminApiRequest::GetClusterLayout(GetClusterLayoutRequest)) + } + Endpoint::UpdateClusterLayout => { + let updates = parse_json_body::(req).await?; + Ok(AdminApiRequest::UpdateClusterLayout(updates)) + } + Endpoint::ApplyClusterLayout => { + let param = parse_json_body::(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, - } => { - let show_secret_key = show_secret_key.map(|x| x == "true").unwrap_or(false); - handle_get_key_info(&self.garage, id, search, show_secret_key).await - } - Endpoint::CreateKey => handle_create_key(&self.garage, req).await, - Endpoint::ImportKey => handle_import_key(&self.garage, req).await, - Endpoint::UpdateKey { id } => handle_update_key(&self.garage, id, req).await, - Endpoint::DeleteKey { id } => handle_delete_key(&self.garage, id).await, - // Buckets - Endpoint::ListBuckets => handle_list_buckets(&self.garage).await, - Endpoint::GetBucketInfo { id, global_alias } => { - handle_get_bucket_info(&self.garage, id, global_alias).await - } - Endpoint::CreateBucket => handle_create_bucket(&self.garage, req).await, - Endpoint::DeleteBucket { id } => handle_delete_bucket(&self.garage, id).await, - Endpoint::UpdateBucket { id } => handle_update_bucket(&self.garage, id, req).await, - // Bucket-key permissions - Endpoint::BucketAllowKey => { - handle_bucket_change_key_perm(&self.garage, req, true).await - } - Endpoint::BucketDenyKey => { - handle_bucket_change_key_perm(&self.garage, req, false).await - } - // Bucket aliasing - Endpoint::GlobalAliasBucket { id, alias } => { - handle_global_alias_bucket(&self.garage, id, alias).await - } - Endpoint::GlobalUnaliasBucket { id, alias } => { - handle_global_unalias_bucket(&self.garage, id, alias).await - } - Endpoint::LocalAliasBucket { + })) + } + Endpoint::CreateKey => { + let req = parse_json_body::(req).await?; + Ok(AdminApiRequest::CreateKey(req)) + } + Endpoint::ImportKey => { + let req = parse_json_body::(req).await?; + Ok(AdminApiRequest::ImportKey(req)) + } + Endpoint::UpdateKey { id } => { + let params = parse_json_body::(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, - access_key_id, - alias, - } => handle_local_alias_bucket(&self.garage, id, access_key_id, alias).await, - Endpoint::LocalUnaliasBucket { + global_alias, + })) + } + Endpoint::CreateBucket => { + let req = parse_json_body::(req).await?; + Ok(AdminApiRequest::CreateBucket(req)) + } + Endpoint::DeleteBucket { id } => { + Ok(AdminApiRequest::DeleteBucket(DeleteBucketRequest { id })) + } + Endpoint::UpdateBucket { id } => { + let params = parse_json_body::(req).await?; + Ok(AdminApiRequest::UpdateBucket(UpdateBucketRequest { id, + params, + })) + } + // Bucket-key permissions + Endpoint::BucketAllowKey => { + let req = parse_json_body::(req).await?; + Ok(AdminApiRequest::BucketAllowKey(BucketAllowKeyRequest(req))) + } + Endpoint::BucketDenyKey => { + let req = parse_json_body::(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, - } => handle_local_unalias_bucket(&self.garage, id, access_key_id, alias).await, - } + }, + )), + _ => unreachable!(), } } diff --git a/src/api/admin/bucket.rs b/src/api/admin/bucket.rs index 593848f0..d62bfa54 100644 --- a/src/api/admin/bucket.rs +++ b/src/api/admin/bucket.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::sync::Arc; -use hyper::{body::Incoming as IncomingBody, Request, Response, StatusCode}; +use async_trait::async_trait; use garage_util::crdt::*; use garage_util::data::*; @@ -18,83 +18,93 @@ use garage_model::s3::object_table::*; use crate::admin::api::ApiBucketKeyPerm; use crate::admin::api::{ - ApiBucketQuotas, BucketKeyPermChangeRequest, BucketLocalAlias, CreateBucketRequest, - GetBucketInfoKey, GetBucketInfoResponse, GetBucketInfoWebsiteResponse, ListBucketsResponseItem, - UpdateBucketRequest, + ApiBucketQuotas, BucketAllowKeyRequest, BucketAllowKeyResponse, BucketDenyKeyRequest, + BucketDenyKeyResponse, BucketKeyPermChangeRequest, BucketLocalAlias, CreateBucketRequest, + CreateBucketResponse, DeleteBucketRequest, DeleteBucketResponse, GetBucketInfoKey, + GetBucketInfoRequest, GetBucketInfoResponse, GetBucketInfoWebsiteResponse, + GlobalAliasBucketRequest, GlobalAliasBucketResponse, GlobalUnaliasBucketRequest, + GlobalUnaliasBucketResponse, ListBucketsRequest, ListBucketsResponse, ListBucketsResponseItem, + LocalAliasBucketRequest, LocalAliasBucketResponse, LocalUnaliasBucketRequest, + LocalUnaliasBucketResponse, UpdateBucketRequest, UpdateBucketResponse, }; -use crate::admin::api_server::ResBody; use crate::admin::error::*; +use crate::admin::EndpointHandler; use crate::common_error::CommonError; -use crate::helpers::*; - -pub async fn handle_list_buckets(garage: &Arc) -> Result, Error> { - let buckets = garage - .bucket_table - .get_range( - &EmptyKey, - None, - Some(DeletedFilter::NotDeleted), - 10000, - EnumerationOrder::Forward, - ) - .await?; - let res = buckets - .into_iter() - .map(|b| { - let state = b.state.as_option().unwrap(); - ListBucketsResponseItem { - id: hex::encode(b.id), - global_aliases: state - .aliases - .items() - .iter() - .filter(|(_, _, a)| *a) - .map(|(n, _, _)| n.to_string()) - .collect::>(), - local_aliases: state - .local_aliases - .items() - .iter() - .filter(|(_, _, a)| *a) - .map(|((k, n), _, _)| BucketLocalAlias { - access_key_id: k.to_string(), - alias: n.to_string(), - }) - .collect::>(), - } - }) - .collect::>(); +#[async_trait] +impl EndpointHandler for ListBucketsRequest { + type Response = ListBucketsResponse; + + async fn handle(self, garage: &Arc) -> Result { + let buckets = garage + .bucket_table + .get_range( + &EmptyKey, + None, + Some(DeletedFilter::NotDeleted), + 10000, + EnumerationOrder::Forward, + ) + .await?; + + let res = buckets + .into_iter() + .map(|b| { + let state = b.state.as_option().unwrap(); + ListBucketsResponseItem { + id: hex::encode(b.id), + global_aliases: state + .aliases + .items() + .iter() + .filter(|(_, _, a)| *a) + .map(|(n, _, _)| n.to_string()) + .collect::>(), + local_aliases: state + .local_aliases + .items() + .iter() + .filter(|(_, _, a)| *a) + .map(|((k, n), _, _)| BucketLocalAlias { + access_key_id: k.to_string(), + alias: n.to_string(), + }) + .collect::>(), + } + }) + .collect::>(); - Ok(json_ok_response(&res)?) + Ok(ListBucketsResponse(res)) + } } -pub async fn handle_get_bucket_info( - garage: &Arc, - id: Option, - global_alias: Option, -) -> Result, Error> { - let bucket_id = match (id, global_alias) { - (Some(id), None) => parse_bucket_id(&id)?, - (None, Some(ga)) => garage - .bucket_helper() - .resolve_global_bucket_name(&ga) - .await? - .ok_or_else(|| HelperError::NoSuchBucket(ga.to_string()))?, - _ => { - return Err(Error::bad_request( - "Either id or globalAlias must be provided (but not both)", - )); - } - }; +#[async_trait] +impl EndpointHandler for GetBucketInfoRequest { + type Response = GetBucketInfoResponse; + + async fn handle(self, garage: &Arc) -> Result { + let bucket_id = match (self.id, self.global_alias) { + (Some(id), None) => parse_bucket_id(&id)?, + (None, Some(ga)) => garage + .bucket_helper() + .resolve_global_bucket_name(&ga) + .await? + .ok_or_else(|| HelperError::NoSuchBucket(ga.to_string()))?, + _ => { + return Err(Error::bad_request( + "Either id or globalAlias must be provided (but not both)", + )); + } + }; - bucket_info_results(garage, bucket_id).await + bucket_info_results(garage, bucket_id).await + } } async fn bucket_info_results( garage: &Arc, bucket_id: Uuid, -) -> Result, Error> { +) -> Result { let bucket = garage .bucket_helper() .get_existing_bucket(bucket_id) @@ -211,181 +221,203 @@ async fn bucket_info_results( }, }; - Ok(json_ok_response(&res)?) + Ok(res) } -pub async fn handle_create_bucket( - garage: &Arc, - req: Request, -) -> Result, Error> { - let req = parse_json_body::(req).await?; +#[async_trait] +impl EndpointHandler for CreateBucketRequest { + type Response = CreateBucketResponse; - let helper = garage.locked_helper().await; + async fn handle(self, garage: &Arc) -> Result { + let helper = garage.locked_helper().await; - if let Some(ga) = &req.global_alias { - if !is_valid_bucket_name(ga) { - return Err(Error::bad_request(format!( - "{}: {}", - ga, INVALID_BUCKET_NAME_MESSAGE - ))); - } + if let Some(ga) = &self.global_alias { + if !is_valid_bucket_name(ga) { + return Err(Error::bad_request(format!( + "{}: {}", + ga, INVALID_BUCKET_NAME_MESSAGE + ))); + } - if let Some(alias) = garage.bucket_alias_table.get(&EmptyKey, ga).await? { - if alias.state.get().is_some() { - return Err(CommonError::BucketAlreadyExists.into()); + if let Some(alias) = garage.bucket_alias_table.get(&EmptyKey, ga).await? { + if alias.state.get().is_some() { + return Err(CommonError::BucketAlreadyExists.into()); + } } } - } - if let Some(la) = &req.local_alias { - if !is_valid_bucket_name(&la.alias) { - return Err(Error::bad_request(format!( - "{}: {}", - la.alias, INVALID_BUCKET_NAME_MESSAGE - ))); - } + if let Some(la) = &self.local_alias { + if !is_valid_bucket_name(&la.alias) { + return Err(Error::bad_request(format!( + "{}: {}", + la.alias, INVALID_BUCKET_NAME_MESSAGE + ))); + } - let key = helper.key().get_existing_key(&la.access_key_id).await?; - let state = key.state.as_option().unwrap(); - if matches!(state.local_aliases.get(&la.alias), Some(_)) { - return Err(Error::bad_request("Local alias already exists")); + let key = helper.key().get_existing_key(&la.access_key_id).await?; + let state = key.state.as_option().unwrap(); + if matches!(state.local_aliases.get(&la.alias), Some(_)) { + return Err(Error::bad_request("Local alias already exists")); + } } - } - - let bucket = Bucket::new(); - garage.bucket_table.insert(&bucket).await?; - if let Some(ga) = &req.global_alias { - helper.set_global_bucket_alias(bucket.id, ga).await?; - } + let bucket = Bucket::new(); + garage.bucket_table.insert(&bucket).await?; - if let Some(la) = &req.local_alias { - helper - .set_local_bucket_alias(bucket.id, &la.access_key_id, &la.alias) - .await?; + if let Some(ga) = &self.global_alias { + helper.set_global_bucket_alias(bucket.id, ga).await?; + } - if la.allow.read || la.allow.write || la.allow.owner { + if let Some(la) = &self.local_alias { helper - .set_bucket_key_permissions( - bucket.id, - &la.access_key_id, - BucketKeyPerm { - timestamp: now_msec(), - allow_read: la.allow.read, - allow_write: la.allow.write, - allow_owner: la.allow.owner, - }, - ) + .set_local_bucket_alias(bucket.id, &la.access_key_id, &la.alias) .await?; + + if la.allow.read || la.allow.write || la.allow.owner { + helper + .set_bucket_key_permissions( + bucket.id, + &la.access_key_id, + BucketKeyPerm { + timestamp: now_msec(), + allow_read: la.allow.read, + allow_write: la.allow.write, + allow_owner: la.allow.owner, + }, + ) + .await?; + } } - } - bucket_info_results(garage, bucket.id).await + Ok(CreateBucketResponse( + bucket_info_results(garage, bucket.id).await?, + )) + } } -pub async fn handle_delete_bucket( - garage: &Arc, - id: String, -) -> Result, Error> { - let helper = garage.locked_helper().await; +#[async_trait] +impl EndpointHandler for DeleteBucketRequest { + type Response = DeleteBucketResponse; - let bucket_id = parse_bucket_id(&id)?; + async fn handle(self, garage: &Arc) -> Result { + let helper = garage.locked_helper().await; - let mut bucket = helper.bucket().get_existing_bucket(bucket_id).await?; - let state = bucket.state.as_option().unwrap(); + let bucket_id = parse_bucket_id(&self.id)?; - // Check bucket is empty - if !helper.bucket().is_bucket_empty(bucket_id).await? { - return Err(CommonError::BucketNotEmpty.into()); - } + let mut bucket = helper.bucket().get_existing_bucket(bucket_id).await?; + let state = bucket.state.as_option().unwrap(); - // --- done checking, now commit --- - // 1. delete authorization from keys that had access - for (key_id, perm) in bucket.authorized_keys() { - if perm.is_any() { - helper - .set_bucket_key_permissions(bucket.id, key_id, BucketKeyPerm::NO_PERMISSIONS) - .await?; + // Check bucket is empty + if !helper.bucket().is_bucket_empty(bucket_id).await? { + return Err(CommonError::BucketNotEmpty.into()); } - } - // 2. delete all local aliases - for ((key_id, alias), _, active) in state.local_aliases.items().iter() { - if *active { - helper - .unset_local_bucket_alias(bucket.id, key_id, alias) - .await?; + + // --- done checking, now commit --- + // 1. delete authorization from keys that had access + for (key_id, perm) in bucket.authorized_keys() { + if perm.is_any() { + helper + .set_bucket_key_permissions(bucket.id, key_id, BucketKeyPerm::NO_PERMISSIONS) + .await?; + } } - } - // 3. delete all global aliases - for (alias, _, active) in state.aliases.items().iter() { - if *active { - helper.purge_global_bucket_alias(bucket.id, alias).await?; + // 2. delete all local aliases + for ((key_id, alias), _, active) in state.local_aliases.items().iter() { + if *active { + helper + .unset_local_bucket_alias(bucket.id, key_id, alias) + .await?; + } + } + // 3. delete all global aliases + for (alias, _, active) in state.aliases.items().iter() { + if *active { + helper.purge_global_bucket_alias(bucket.id, alias).await?; + } } - } - // 4. delete bucket - bucket.state = Deletable::delete(); - garage.bucket_table.insert(&bucket).await?; + // 4. delete bucket + bucket.state = Deletable::delete(); + garage.bucket_table.insert(&bucket).await?; - Ok(Response::builder() - .status(StatusCode::NO_CONTENT) - .body(empty_body())?) + Ok(DeleteBucketResponse) + } } -pub async fn handle_update_bucket( - garage: &Arc, - id: String, - req: Request, -) -> Result, Error> { - let req = parse_json_body::(req).await?; - let bucket_id = parse_bucket_id(&id)?; +#[async_trait] +impl EndpointHandler for UpdateBucketRequest { + type Response = UpdateBucketResponse; - let mut bucket = garage - .bucket_helper() - .get_existing_bucket(bucket_id) - .await?; + async fn handle(self, garage: &Arc) -> Result { + let bucket_id = parse_bucket_id(&self.id)?; - let state = bucket.state.as_option_mut().unwrap(); - - if let Some(wa) = req.website_access { - if wa.enabled { - state.website_config.update(Some(WebsiteConfig { - index_document: wa.index_document.ok_or_bad_request( - "Please specify indexDocument when enabling website access.", - )?, - error_document: wa.error_document, - })); - } else { - if wa.index_document.is_some() || wa.error_document.is_some() { - return Err(Error::bad_request( - "Cannot specify indexDocument or errorDocument when disabling website access.", - )); + let mut bucket = garage + .bucket_helper() + .get_existing_bucket(bucket_id) + .await?; + + let state = bucket.state.as_option_mut().unwrap(); + + if let Some(wa) = self.params.website_access { + if wa.enabled { + state.website_config.update(Some(WebsiteConfig { + index_document: wa.index_document.ok_or_bad_request( + "Please specify indexDocument when enabling website access.", + )?, + error_document: wa.error_document, + })); + } else { + if wa.index_document.is_some() || wa.error_document.is_some() { + return Err(Error::bad_request( + "Cannot specify indexDocument or errorDocument when disabling website access.", + )); + } + state.website_config.update(None); } - state.website_config.update(None); } - } - if let Some(q) = req.quotas { - state.quotas.update(BucketQuotas { - max_size: q.max_size, - max_objects: q.max_objects, - }); - } + if let Some(q) = self.params.quotas { + state.quotas.update(BucketQuotas { + max_size: q.max_size, + max_objects: q.max_objects, + }); + } - garage.bucket_table.insert(&bucket).await?; + garage.bucket_table.insert(&bucket).await?; - bucket_info_results(garage, bucket_id).await + Ok(UpdateBucketResponse( + bucket_info_results(garage, bucket_id).await?, + )) + } } // ---- BUCKET/KEY PERMISSIONS ---- +#[async_trait] +impl EndpointHandler for BucketAllowKeyRequest { + type Response = BucketAllowKeyResponse; + + async fn handle(self, garage: &Arc) -> Result { + let res = handle_bucket_change_key_perm(garage, self.0, true).await?; + Ok(BucketAllowKeyResponse(res)) + } +} + +#[async_trait] +impl EndpointHandler for BucketDenyKeyRequest { + type Response = BucketDenyKeyResponse; + + async fn handle(self, garage: &Arc) -> Result { + let res = handle_bucket_change_key_perm(garage, self.0, false).await?; + Ok(BucketDenyKeyResponse(res)) + } +} + pub async fn handle_bucket_change_key_perm( garage: &Arc, - req: Request, + req: BucketKeyPermChangeRequest, new_perm_flag: bool, -) -> Result, Error> { - let req = parse_json_body::(req).await?; - +) -> Result { let helper = garage.locked_helper().await; let bucket_id = parse_bucket_id(&req.bucket_id)?; @@ -420,66 +452,80 @@ pub async fn handle_bucket_change_key_perm( // ---- BUCKET ALIASES ---- -pub async fn handle_global_alias_bucket( - garage: &Arc, - bucket_id: String, - alias: String, -) -> Result, Error> { - let bucket_id = parse_bucket_id(&bucket_id)?; +#[async_trait] +impl EndpointHandler for GlobalAliasBucketRequest { + type Response = GlobalAliasBucketResponse; - let helper = garage.locked_helper().await; + async fn handle(self, garage: &Arc) -> Result { + let bucket_id = parse_bucket_id(&self.id)?; - helper.set_global_bucket_alias(bucket_id, &alias).await?; + let helper = garage.locked_helper().await; - bucket_info_results(garage, bucket_id).await + helper + .set_global_bucket_alias(bucket_id, &self.alias) + .await?; + + Ok(GlobalAliasBucketResponse( + bucket_info_results(garage, bucket_id).await?, + )) + } } -pub async fn handle_global_unalias_bucket( - garage: &Arc, - bucket_id: String, - alias: String, -) -> Result, Error> { - let bucket_id = parse_bucket_id(&bucket_id)?; +#[async_trait] +impl EndpointHandler for GlobalUnaliasBucketRequest { + type Response = GlobalUnaliasBucketResponse; - let helper = garage.locked_helper().await; + async fn handle(self, garage: &Arc) -> Result { + let bucket_id = parse_bucket_id(&self.id)?; + + let helper = garage.locked_helper().await; - helper.unset_global_bucket_alias(bucket_id, &alias).await?; + helper + .unset_global_bucket_alias(bucket_id, &self.alias) + .await?; - bucket_info_results(garage, bucket_id).await + Ok(GlobalUnaliasBucketResponse( + bucket_info_results(garage, bucket_id).await?, + )) + } } -pub async fn handle_local_alias_bucket( - garage: &Arc, - bucket_id: String, - access_key_id: String, - alias: String, -) -> Result, Error> { - let bucket_id = parse_bucket_id(&bucket_id)?; +#[async_trait] +impl EndpointHandler for LocalAliasBucketRequest { + type Response = LocalAliasBucketResponse; - let helper = garage.locked_helper().await; + async fn handle(self, garage: &Arc) -> Result { + let bucket_id = parse_bucket_id(&self.id)?; - helper - .set_local_bucket_alias(bucket_id, &access_key_id, &alias) - .await?; + let helper = garage.locked_helper().await; - bucket_info_results(garage, bucket_id).await + helper + .set_local_bucket_alias(bucket_id, &self.access_key_id, &self.alias) + .await?; + + Ok(LocalAliasBucketResponse( + bucket_info_results(garage, bucket_id).await?, + )) + } } -pub async fn handle_local_unalias_bucket( - garage: &Arc, - bucket_id: String, - access_key_id: String, - alias: String, -) -> Result, Error> { - let bucket_id = parse_bucket_id(&bucket_id)?; +#[async_trait] +impl EndpointHandler for LocalUnaliasBucketRequest { + type Response = LocalUnaliasBucketResponse; - let helper = garage.locked_helper().await; + async fn handle(self, garage: &Arc) -> Result { + let bucket_id = parse_bucket_id(&self.id)?; - helper - .unset_local_bucket_alias(bucket_id, &access_key_id, &alias) - .await?; + let helper = garage.locked_helper().await; - bucket_info_results(garage, bucket_id).await + helper + .unset_local_bucket_alias(bucket_id, &self.access_key_id, &self.alias) + .await?; + + Ok(LocalUnaliasBucketResponse( + bucket_info_results(garage, bucket_id).await?, + )) + } } // ---- HELPER ---- diff --git a/src/api/admin/cluster.rs b/src/api/admin/cluster.rs index 11753509..c7eb7e7d 100644 --- a/src/api/admin/cluster.rs +++ b/src/api/admin/cluster.rs @@ -2,7 +2,6 @@ use std::collections::HashMap; use std::sync::Arc; use async_trait::async_trait; -use hyper::{body::Incoming as IncomingBody, Request, Response}; use garage_util::crdt::*; use garage_util::data::*; @@ -14,14 +13,13 @@ use garage_model::garage::Garage; use crate::admin::api::{ ApplyClusterLayoutRequest, ApplyClusterLayoutResponse, ConnectClusterNodeResponse, ConnectClusterNodesRequest, ConnectClusterNodesResponse, FreeSpaceResp, - GetClusterHealthRequest, GetClusterHealthResponse, GetClusterLayoutResponse, - GetClusterStatusRequest, GetClusterStatusResponse, NodeResp, NodeRoleChange, - NodeRoleChangeEnum, NodeRoleResp, UpdateClusterLayoutRequest, + GetClusterHealthRequest, GetClusterHealthResponse, GetClusterLayoutRequest, + GetClusterLayoutResponse, GetClusterStatusRequest, GetClusterStatusResponse, NodeResp, + NodeRoleChange, NodeRoleChangeEnum, NodeRoleResp, RevertClusterLayoutRequest, + RevertClusterLayoutResponse, UpdateClusterLayoutRequest, UpdateClusterLayoutResponse, }; -use crate::admin::api_server::ResBody; use crate::admin::error::*; use crate::admin::EndpointHandler; -use crate::helpers::{json_ok_response, parse_json_body}; #[async_trait] impl EndpointHandler for GetClusterStatusRequest { @@ -149,17 +147,6 @@ impl EndpointHandler for GetClusterHealthRequest { } } -pub async fn handle_connect_cluster_nodes( - garage: &Arc, - req: Request, -) -> Result, Error> { - let req = parse_json_body::(req).await?; - - let res = req.handle(garage).await?; - - Ok(json_ok_response(&res)?) -} - #[async_trait] impl EndpointHandler for ConnectClusterNodesRequest { type Response = ConnectClusterNodesResponse; @@ -183,10 +170,15 @@ impl EndpointHandler for ConnectClusterNodesRequest { } } -pub async fn handle_get_cluster_layout(garage: &Arc) -> Result, Error> { - let res = format_cluster_layout(garage.system.cluster_layout().inner()); +#[async_trait] +impl EndpointHandler for GetClusterLayoutRequest { + type Response = GetClusterLayoutResponse; - Ok(json_ok_response(&res)?) + async fn handle(self, garage: &Arc) -> Result { + Ok(format_cluster_layout( + garage.system.cluster_layout().inner(), + )) + } } fn format_cluster_layout(layout: &layout::LayoutHistory) -> GetClusterLayoutResponse { @@ -238,85 +230,87 @@ fn format_cluster_layout(layout: &layout::LayoutHistory) -> GetClusterLayoutResp // ---- update functions ---- -pub async fn handle_update_cluster_layout( - garage: &Arc, - req: Request, -) -> Result, Error> { - let updates = parse_json_body::(req).await?; - - let mut layout = garage.system.cluster_layout().inner().clone(); - - let mut roles = layout.current().roles.clone(); - roles.merge(&layout.staging.get().roles); - - for change in updates.0 { - let node = hex::decode(&change.id).ok_or_bad_request("Invalid node identifier")?; - let node = Uuid::try_from(&node).ok_or_bad_request("Invalid node identifier")?; - - let new_role = match change.action { - NodeRoleChangeEnum::Remove { remove: true } => None, - NodeRoleChangeEnum::Update { - zone, - capacity, - tags, - } => Some(layout::NodeRole { - zone, - capacity, - tags, - }), - _ => return Err(Error::bad_request("Invalid layout change")), - }; +#[async_trait] +impl EndpointHandler for UpdateClusterLayoutRequest { + type Response = UpdateClusterLayoutResponse; + + async fn handle(self, garage: &Arc) -> Result { + let mut layout = garage.system.cluster_layout().inner().clone(); + + let mut roles = layout.current().roles.clone(); + roles.merge(&layout.staging.get().roles); + + for change in self.0 { + let node = hex::decode(&change.id).ok_or_bad_request("Invalid node identifier")?; + let node = Uuid::try_from(&node).ok_or_bad_request("Invalid node identifier")?; + + let new_role = match change.action { + NodeRoleChangeEnum::Remove { remove: true } => None, + NodeRoleChangeEnum::Update { + zone, + capacity, + tags, + } => Some(layout::NodeRole { + zone, + capacity, + tags, + }), + _ => return Err(Error::bad_request("Invalid layout change")), + }; + + layout + .staging + .get_mut() + .roles + .merge(&roles.update_mutator(node, layout::NodeRoleV(new_role))); + } - layout - .staging - .get_mut() - .roles - .merge(&roles.update_mutator(node, layout::NodeRoleV(new_role))); + garage + .system + .layout_manager + .update_cluster_layout(&layout) + .await?; + + let res = format_cluster_layout(&layout); + Ok(UpdateClusterLayoutResponse(res)) } +} - garage - .system - .layout_manager - .update_cluster_layout(&layout) - .await?; +#[async_trait] +impl EndpointHandler for ApplyClusterLayoutRequest { + type Response = ApplyClusterLayoutResponse; - let res = format_cluster_layout(&layout); - Ok(json_ok_response(&res)?) -} + async fn handle(self, garage: &Arc) -> Result { + let layout = garage.system.cluster_layout().inner().clone(); + let (layout, msg) = layout.apply_staged_changes(Some(self.version))?; + + garage + .system + .layout_manager + .update_cluster_layout(&layout) + .await?; -pub async fn handle_apply_cluster_layout( - garage: &Arc, - req: Request, -) -> Result, Error> { - let param = parse_json_body::(req).await?; - - let layout = garage.system.cluster_layout().inner().clone(); - let (layout, msg) = layout.apply_staged_changes(Some(param.version))?; - - garage - .system - .layout_manager - .update_cluster_layout(&layout) - .await?; - - let res = ApplyClusterLayoutResponse { - message: msg, - layout: format_cluster_layout(&layout), - }; - Ok(json_ok_response(&res)?) + Ok(ApplyClusterLayoutResponse { + message: msg, + layout: format_cluster_layout(&layout), + }) + } } -pub async fn handle_revert_cluster_layout( - garage: &Arc, -) -> Result, Error> { - let layout = garage.system.cluster_layout().inner().clone(); - let layout = layout.revert_staged_changes()?; - garage - .system - .layout_manager - .update_cluster_layout(&layout) - .await?; - - let res = format_cluster_layout(&layout); - Ok(json_ok_response(&res)?) +#[async_trait] +impl EndpointHandler for RevertClusterLayoutRequest { + type Response = RevertClusterLayoutResponse; + + async fn handle(self, garage: &Arc) -> Result { + let layout = garage.system.cluster_layout().inner().clone(); + let layout = layout.revert_staged_changes()?; + garage + .system + .layout_manager + .update_cluster_layout(&layout) + .await?; + + let res = format_cluster_layout(&layout); + Ok(RevertClusterLayoutResponse(res)) + } } diff --git a/src/api/admin/key.rs b/src/api/admin/key.rs index 96ce3518..8161672f 100644 --- a/src/api/admin/key.rs +++ b/src/api/admin/key.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::sync::Arc; -use hyper::{body::Incoming as IncomingBody, Request, Response, StatusCode}; +use async_trait::async_trait; use garage_table::*; @@ -9,138 +9,149 @@ use garage_model::garage::Garage; use garage_model::key_table::*; use crate::admin::api::{ - ApiBucketKeyPerm, CreateKeyRequest, GetKeyInfoResponse, ImportKeyRequest, - KeyInfoBucketResponse, KeyPerm, ListKeysResponseItem, UpdateKeyRequest, + ApiBucketKeyPerm, CreateKeyRequest, CreateKeyResponse, DeleteKeyRequest, DeleteKeyResponse, + GetKeyInfoRequest, GetKeyInfoResponse, ImportKeyRequest, ImportKeyResponse, + KeyInfoBucketResponse, KeyPerm, ListKeysRequest, ListKeysResponse, ListKeysResponseItem, + UpdateKeyRequest, UpdateKeyResponse, }; -use crate::admin::api_server::ResBody; use crate::admin::error::*; -use crate::helpers::*; - -pub async fn handle_list_keys(garage: &Arc) -> Result, Error> { - let res = garage - .key_table - .get_range( - &EmptyKey, - None, - Some(KeyFilter::Deleted(DeletedFilter::NotDeleted)), - 10000, - EnumerationOrder::Forward, - ) - .await? - .iter() - .map(|k| ListKeysResponseItem { - id: k.key_id.to_string(), - name: k.params().unwrap().name.get().clone(), - }) - .collect::>(); +use crate::admin::EndpointHandler; + +#[async_trait] +impl EndpointHandler for ListKeysRequest { + type Response = ListKeysResponse; + + async fn handle(self, garage: &Arc) -> Result { + let res = garage + .key_table + .get_range( + &EmptyKey, + None, + Some(KeyFilter::Deleted(DeletedFilter::NotDeleted)), + 10000, + EnumerationOrder::Forward, + ) + .await? + .iter() + .map(|k| ListKeysResponseItem { + id: k.key_id.to_string(), + name: k.params().unwrap().name.get().clone(), + }) + .collect::>(); - Ok(json_ok_response(&res)?) + Ok(ListKeysResponse(res)) + } } -pub async fn handle_get_key_info( - garage: &Arc, - id: Option, - search: Option, - show_secret_key: bool, -) -> Result, Error> { - let key = if let Some(id) = id { - garage.key_helper().get_existing_key(&id).await? - } else if let Some(search) = search { - garage - .key_helper() - .get_existing_matching_key(&search) - .await? - } else { - unreachable!(); - }; +#[async_trait] +impl EndpointHandler for GetKeyInfoRequest { + type Response = GetKeyInfoResponse; + + async fn handle(self, garage: &Arc) -> Result { + let key = if let Some(id) = self.id { + garage.key_helper().get_existing_key(&id).await? + } else if let Some(search) = self.search { + garage + .key_helper() + .get_existing_matching_key(&search) + .await? + } else { + unreachable!(); + }; - key_info_results(garage, key, show_secret_key).await + Ok(key_info_results(garage, key, self.show_secret_key).await?) + } } -pub async fn handle_create_key( - garage: &Arc, - req: Request, -) -> Result, Error> { - let req = parse_json_body::(req).await?; +#[async_trait] +impl EndpointHandler for CreateKeyRequest { + type Response = CreateKeyResponse; - let key = Key::new(req.name.as_deref().unwrap_or("Unnamed key")); - garage.key_table.insert(&key).await?; + async fn handle(self, garage: &Arc) -> Result { + let key = Key::new(self.name.as_deref().unwrap_or("Unnamed key")); + garage.key_table.insert(&key).await?; - key_info_results(garage, key, true).await + Ok(CreateKeyResponse( + key_info_results(garage, key, true).await?, + )) + } } -pub async fn handle_import_key( - garage: &Arc, - req: Request, -) -> Result, Error> { - let req = parse_json_body::(req).await?; +#[async_trait] +impl EndpointHandler for ImportKeyRequest { + type Response = ImportKeyResponse; - let prev_key = garage.key_table.get(&EmptyKey, &req.access_key_id).await?; - if prev_key.is_some() { - return Err(Error::KeyAlreadyExists(req.access_key_id.to_string())); - } + async fn handle(self, garage: &Arc) -> Result { + let prev_key = garage.key_table.get(&EmptyKey, &self.access_key_id).await?; + if prev_key.is_some() { + return Err(Error::KeyAlreadyExists(self.access_key_id.to_string())); + } - let imported_key = Key::import( - &req.access_key_id, - &req.secret_access_key, - req.name.as_deref().unwrap_or("Imported key"), - ) - .ok_or_bad_request("Invalid key format")?; - garage.key_table.insert(&imported_key).await?; + let imported_key = Key::import( + &self.access_key_id, + &self.secret_access_key, + self.name.as_deref().unwrap_or("Imported key"), + ) + .ok_or_bad_request("Invalid key format")?; + garage.key_table.insert(&imported_key).await?; - key_info_results(garage, imported_key, false).await + Ok(ImportKeyResponse( + key_info_results(garage, imported_key, false).await?, + )) + } } -pub async fn handle_update_key( - garage: &Arc, - id: String, - req: Request, -) -> Result, Error> { - let req = parse_json_body::(req).await?; +#[async_trait] +impl EndpointHandler for UpdateKeyRequest { + type Response = UpdateKeyResponse; - let mut key = garage.key_helper().get_existing_key(&id).await?; + async fn handle(self, garage: &Arc) -> Result { + let mut key = garage.key_helper().get_existing_key(&self.id).await?; - let key_state = key.state.as_option_mut().unwrap(); + let key_state = key.state.as_option_mut().unwrap(); - if let Some(new_name) = req.name { - key_state.name.update(new_name); - } - if let Some(allow) = req.allow { - if allow.create_bucket { - key_state.allow_create_bucket.update(true); + if let Some(new_name) = self.params.name { + key_state.name.update(new_name); } - } - if let Some(deny) = req.deny { - if deny.create_bucket { - key_state.allow_create_bucket.update(false); + if let Some(allow) = self.params.allow { + if allow.create_bucket { + key_state.allow_create_bucket.update(true); + } + } + if let Some(deny) = self.params.deny { + if deny.create_bucket { + key_state.allow_create_bucket.update(false); + } } - } - garage.key_table.insert(&key).await?; + garage.key_table.insert(&key).await?; - key_info_results(garage, key, false).await + Ok(UpdateKeyResponse( + key_info_results(garage, key, false).await?, + )) + } } -pub async fn handle_delete_key( - garage: &Arc, - id: String, -) -> Result, Error> { - let helper = garage.locked_helper().await; +#[async_trait] +impl EndpointHandler for DeleteKeyRequest { + type Response = DeleteKeyResponse; + + async fn handle(self, garage: &Arc) -> Result { + let helper = garage.locked_helper().await; - let mut key = helper.key().get_existing_key(&id).await?; + let mut key = helper.key().get_existing_key(&self.id).await?; - helper.delete_key(&mut key).await?; + helper.delete_key(&mut key).await?; - Ok(Response::builder() - .status(StatusCode::NO_CONTENT) - .body(empty_body())?) + Ok(DeleteKeyResponse) + } } async fn key_info_results( garage: &Arc, key: Key, show_secret: bool, -) -> Result, Error> { +) -> Result { let mut relevant_buckets = HashMap::new(); let key_state = key.state.as_option().unwrap(); @@ -211,5 +222,5 @@ async fn key_info_results( .collect::>(), }; - Ok(json_ok_response(&res)?) + Ok(res) } -- cgit v1.2.3 From c99bfe69ea19497895d32669fd15c689b86035d8 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 28 Jan 2025 15:12:03 +0100 Subject: admin api: new router_v2 with unified path syntax --- src/api/Cargo.toml | 1 + src/api/admin/api.rs | 31 +++-- src/api/admin/api_server.rs | 296 +++++--------------------------------------- src/api/admin/bucket.rs | 4 +- src/api/admin/key.rs | 6 +- src/api/admin/mod.rs | 11 +- src/api/admin/router_v1.rs | 7 +- src/api/admin/router_v2.rs | 169 +++++++++++++++++++++++++ src/api/admin/special.rs | 129 +++++++++++++++++++ src/api/generic_server.rs | 3 +- src/api/k2v/api_server.rs | 5 +- src/api/router_macros.rs | 71 +++++++++++ src/api/s3/api_server.rs | 5 +- 13 files changed, 447 insertions(+), 291 deletions(-) create mode 100644 src/api/admin/router_v2.rs create mode 100644 src/api/admin/special.rs (limited to 'src/api') 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) -> Result { 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, pub allow: Option, @@ -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, pub quotas: Option, } @@ -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, } +enum Endpoint { + Old(endpoint_v1::Endpoint), + New(String), +} + impl AdminApiServer { pub fn new( garage: Arc, @@ -67,130 +72,6 @@ impl AdminApiServer { .await } - fn handle_options(&self, _req: &Request) -> Result, 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, - ) -> Result, Error> { - let query_params: HashMap = 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 { - // 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, 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, Error> { #[cfg(feature = "metrics")] { @@ -231,9 +112,13 @@ impl ApiHandler for AdminApiServer { fn parse_endpoint(&self, req: &Request) -> Result { 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, endpoint: Endpoint, ) -> Result, 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, -) -> Result { - match endpoint { - Endpoint::GetClusterStatus => { - Ok(AdminApiRequest::GetClusterStatus(GetClusterStatusRequest)) - } - Endpoint::GetClusterHealth => { - Ok(AdminApiRequest::GetClusterHealth(GetClusterHealthRequest)) - } - Endpoint::ConnectClusterNodes => { - let req = parse_json_body::(req).await?; - Ok(AdminApiRequest::ConnectClusterNodes(req)) - } - // Layout - Endpoint::GetClusterLayout => { - Ok(AdminApiRequest::GetClusterLayout(GetClusterLayoutRequest)) - } - Endpoint::UpdateClusterLayout => { - let updates = parse_json_body::(req).await?; - Ok(AdminApiRequest::UpdateClusterLayout(updates)) - } - Endpoint::ApplyClusterLayout => { - let param = parse_json_body::(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::(req).await?; - Ok(AdminApiRequest::CreateKey(req)) - } - Endpoint::ImportKey => { - let req = parse_json_body::(req).await?; - Ok(AdminApiRequest::ImportKey(req)) - } - Endpoint::UpdateKey { id } => { - let params = parse_json_body::(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::(req).await?; - Ok(AdminApiRequest::CreateBucket(req)) - } - Endpoint::DeleteBucket { id } => { - Ok(AdminApiRequest::DeleteBucket(DeleteBucketRequest { id })) - } - Endpoint::UpdateBucket { id } => { - let params = parse_json_body::(req).await?; - Ok(AdminApiRequest::UpdateBucket(UpdateBucketRequest { - id, - params, - })) - } - // Bucket-key permissions - Endpoint::BucketAllowKey => { - let req = parse_json_body::(req).await?; - Ok(AdminApiRequest::BucketAllowKey(BucketAllowKeyRequest(req))) - } - Endpoint::BucketDenyKey => { - let req = parse_json_body::(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) -> Result; } 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(req: Request) -> Result { + 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 { + 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; + + async fn handle(self, _garage: &Arc) -> Result, 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; + + async fn handle(self, garage: &Arc) -> Result, 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, domain: &str) -> Result { + // 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; + + async fn handle(self, garage: &Arc) -> Result, 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<'_>) { -- cgit v1.2.3 From af1a53083452e7953736261db57aea4a68aa4278 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 28 Jan 2025 15:44:14 +0100 Subject: admin api: refactor using macro --- src/api/admin/api.rs | 174 ++++++++++---------------------------------- src/api/admin/api_server.rs | 18 +++-- src/api/admin/macros.rs | 58 +++++++++++++++ src/api/admin/mod.rs | 1 + src/api/admin/router_v2.rs | 2 +- src/api/generic_server.rs | 2 +- src/api/k2v/api_server.rs | 4 +- src/api/s3/api_server.rs | 4 +- 8 files changed, 113 insertions(+), 150 deletions(-) create mode 100644 src/api/admin/macros.rs (limited to 'src/api') diff --git a/src/api/admin/api.rs b/src/api/admin/api.rs index b0ab058a..c8fad95b 100644 --- a/src/api/admin/api.rs +++ b/src/api/admin/api.rs @@ -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) -> Result { - 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 diff --git a/src/api/admin/api_server.rs b/src/api/admin/api_server.rs index b235dafc..e00f17c4 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::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) -> Result { 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, endpoint: Endpoint, ) -> Result, 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()), } } diff --git a/src/api/admin/macros.rs b/src/api/admin/macros.rs new file mode 100644 index 00000000..a12dc40b --- /dev/null +++ b/src/api/admin/macros.rs @@ -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) -> Result { + 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; diff --git a/src/api/admin/mod.rs b/src/api/admin/mod.rs index f4c37298..86f5bcac 100644 --- a/src/api/admin/mod.rs +++ b/src/api/admin/mod.rs @@ -1,5 +1,6 @@ pub mod api_server; mod error; +mod macros; pub mod api; mod router_v0; diff --git a/src/api/admin/router_v2.rs b/src/api/admin/router_v2.rs index 9d203500..f9a976c4 100644 --- a/src/api/admin/router_v2.rs +++ b/src/api/admin/router_v2.rs @@ -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(req: Request) -> Result { + pub async fn from_request(req: Request) -> Result { let uri = req.uri().clone(); let path = uri.path(); let query = uri.query(); diff --git a/src/api/generic_server.rs b/src/api/generic_server.rs index ce2ff7b7..5a9b29eb 100644 --- a/src/api/generic_server.rs +++ b/src/api/generic_server.rs @@ -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<'_>); } diff --git a/src/api/k2v/api_server.rs b/src/api/k2v/api_server.rs index 35931914..863452e6 100644 --- a/src/api/k2v/api_server.rs +++ b/src/api/k2v/api_server.rs @@ -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<'_>) { diff --git a/src/api/s3/api_server.rs b/src/api/s3/api_server.rs index 3820ad8f..2b638b15 100644 --- a/src/api/s3/api_server.rs +++ b/src/api/s3/api_server.rs @@ -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<'_>) { -- cgit v1.2.3 From 5037b97dd41cb668289708384c13006f5db2afd7 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 28 Jan 2025 15:59:32 +0100 Subject: admin api: add compatibility from v1/ to v2/ --- src/api/admin/api_server.rs | 6 +- src/api/admin/router_v1.rs | 10 --- src/api/admin/router_v2.rs | 163 ++++++++++++++++++++++++++++++-------------- src/api/router_macros.rs | 4 +- 4 files changed, 115 insertions(+), 68 deletions(-) (limited to 'src/api') diff --git a/src/api/admin/api_server.rs b/src/api/admin/api_server.rs index e00f17c4..2f2e3284 100644 --- a/src/api/admin/api_server.rs +++ b/src/api/admin/api_server.rs @@ -127,12 +127,10 @@ impl ApiHandler for AdminApiServer { req: Request, endpoint: Endpoint, ) -> Result, Error> { - let auth_header = req.headers().get(AUTHORIZATION).clone(); + let auth_header = req.headers().get(AUTHORIZATION).cloned(); let request = match endpoint { - Endpoint::Old(endpoint_v1) => { - todo!() // TODO: convert from old semantics, if possible - } + Endpoint::Old(endpoint_v1) => AdminApiRequest::from_v1(endpoint_v1, req).await?, Endpoint::New(_) => AdminApiRequest::from_request(req).await?, }; diff --git a/src/api/admin/router_v1.rs b/src/api/admin/router_v1.rs index d69675cc..7e738145 100644 --- a/src/api/admin/router_v1.rs +++ b/src/api/admin/router_v1.rs @@ -4,7 +4,6 @@ use hyper::{Method, Request}; use crate::admin::error::*; use crate::admin::router_v0; -use crate::admin::Authorization; use crate::router_macros::*; router_match! {@func @@ -205,15 +204,6 @@ impl Endpoint { ))), } } - /// 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! { diff --git a/src/api/admin/router_v2.rs b/src/api/admin/router_v2.rs index f9a976c4..e0c54f0e 100644 --- a/src/api/admin/router_v2.rs +++ b/src/api/admin/router_v2.rs @@ -6,7 +6,7 @@ use paste::paste; use crate::admin::api::*; use crate::admin::error::*; -//use crate::admin::router_v1; +use crate::admin::router_v1; use crate::admin::Authorization; use crate::helpers::*; use crate::router_macros::*; @@ -67,82 +67,141 @@ impl AdminApiRequest { 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 { - 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 { + /// Some endpoints work exactly the same in their v2/ version as they did in their v1/ version. + /// For these endpoints, we can convert a v1/ call to its equivalent as if it was made using + /// its v2/ URL. + pub async fn from_v1( + v1_endpoint: router_v1::Endpoint, + req: Request, + ) -> Result { + use router_v1::Endpoint; + + match v1_endpoint { + Endpoint::GetClusterStatus => { + Ok(AdminApiRequest::GetClusterStatus(GetClusterStatusRequest)) + } + Endpoint::GetClusterHealth => { + Ok(AdminApiRequest::GetClusterHealth(GetClusterHealthRequest)) + } + Endpoint::ConnectClusterNodes => { + let req = parse_json_body::(req).await?; + Ok(AdminApiRequest::ConnectClusterNodes(req)) + } + + // Layout + Endpoint::GetClusterLayout => { + Ok(AdminApiRequest::GetClusterLayout(GetClusterLayoutRequest)) + } + Endpoint::UpdateClusterLayout => { + let updates = parse_json_body::(req).await?; + Ok(AdminApiRequest::UpdateClusterLayout(updates)) + } + Endpoint::ApplyClusterLayout => { + let param = parse_json_body::(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: Some("true".into()), - }), - router_v0::Endpoint::DeleteKey { id } => Ok(Self::DeleteKey { id }), - // - UpdateKey: response format changed (secret key no longer returned) + 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::(req).await?; + Ok(AdminApiRequest::CreateKey(req)) + } + Endpoint::ImportKey => { + let req = parse_json_body::(req).await?; + Ok(AdminApiRequest::ImportKey(req)) + } + Endpoint::UpdateKey { id } => { + let body = parse_json_body::(req).await?; + Ok(AdminApiRequest::UpdateKey(UpdateKeyRequest { id, body })) + } + Endpoint::DeleteKey { id } => Ok(AdminApiRequest::DeleteKey(DeleteKeyRequest { id })), - // Bucket endpoints - router_v0::Endpoint::GetBucketInfo { id, global_alias } => { - Ok(Self::GetBucketInfo { id, global_alias }) + // 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::(req).await?; + Ok(AdminApiRequest::CreateBucket(req)) + } + Endpoint::DeleteBucket { id } => { + Ok(AdminApiRequest::DeleteBucket(DeleteBucketRequest { id })) + } + Endpoint::UpdateBucket { id } => { + let body = parse_json_body::(req).await?; + Ok(AdminApiRequest::UpdateBucket(UpdateBucketRequest { + id, + body, + })) } - 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 }) + Endpoint::BucketAllowKey => { + let req = parse_json_body::(req).await?; + Ok(AdminApiRequest::BucketAllowKey(BucketAllowKeyRequest(req))) } - router_v0::Endpoint::GlobalUnaliasBucket { id, alias } => { - Ok(Self::GlobalUnaliasBucket { id, alias }) + Endpoint::BucketDenyKey => { + let req = parse_json_body::(req).await?; + Ok(AdminApiRequest::BucketDenyKey(BucketDenyKeyRequest(req))) } - router_v0::Endpoint::LocalAliasBucket { + // 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(Self::LocalAliasBucket { - id, + } => Ok(AdminApiRequest::LocalAliasBucket(LocalAliasBucketRequest { access_key_id, - alias, - }), - router_v0::Endpoint::LocalUnaliasBucket { id, - access_key_id, alias, - } => Ok(Self::LocalUnaliasBucket { + })), + Endpoint::LocalUnaliasBucket { id, access_key_id, alias, - }), + } => Ok(AdminApiRequest::LocalUnaliasBucket( + LocalUnaliasBucketRequest { + access_key_id, + 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() + "v1/ endpoint is no longer supported: {}", + v1_endpoint.name() ))), } } - */ + /// Get the kind of authorization which is required to perform the operation. pub fn authorization_type(&self) -> Authorization { match self { diff --git a/src/api/router_macros.rs b/src/api/router_macros.rs index acbe097c..e8c99909 100644 --- a/src/api/router_macros.rs +++ b/src/api/router_macros.rs @@ -165,8 +165,8 @@ macro_rules! router_match { .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 + // extract and parse optional query parameter + // using provided value as default if paramter is missing $query.$param.take().map(|x| x .parse() .map_err(|_| Error::bad_request("Failed to parse query parameter"))) -- cgit v1.2.3 From ed58f8b0fe3c44eac7416b3aaa444d1b568f8918 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 28 Jan 2025 16:18:48 +0100 Subject: admin api: update semantics of some endpoints, and update doc --- src/api/admin/api.rs | 12 ++++++++---- src/api/admin/api_server.rs | 4 ++-- src/api/admin/bucket.rs | 8 ++++---- src/api/admin/macros.rs | 2 +- src/api/admin/router_v2.rs | 44 +++++++++++++++++++++++++++++--------------- 5 files changed, 44 insertions(+), 26 deletions(-) (limited to 'src/api') diff --git a/src/api/admin/api.rs b/src/api/admin/api.rs index c8fad95b..457863e0 100644 --- a/src/api/admin/api.rs +++ b/src/api/admin/api.rs @@ -500,8 +500,9 @@ pub struct BucketDenyKeyResponse(pub GetBucketInfoResponse); // ---- GlobalAliasBucket ---- +#[derive(Deserialize)] pub struct GlobalAliasBucketRequest { - pub id: String, + pub bucket_id: String, pub alias: String, } @@ -510,8 +511,9 @@ pub struct GlobalAliasBucketResponse(pub GetBucketInfoResponse); // ---- GlobalUnaliasBucket ---- +#[derive(Deserialize)] pub struct GlobalUnaliasBucketRequest { - pub id: String, + pub bucket_id: String, pub alias: String, } @@ -520,8 +522,9 @@ pub struct GlobalUnaliasBucketResponse(pub GetBucketInfoResponse); // ---- LocalAliasBucket ---- +#[derive(Deserialize)] pub struct LocalAliasBucketRequest { - pub id: String, + pub bucket_id: String, pub access_key_id: String, pub alias: String, } @@ -531,8 +534,9 @@ pub struct LocalAliasBucketResponse(pub GetBucketInfoResponse); // ---- LocalUnaliasBucket ---- +#[derive(Deserialize)] pub struct LocalUnaliasBucketRequest { - pub id: String, + pub bucket_id: String, pub access_key_id: String, pub alias: String, } diff --git a/src/api/admin/api_server.rs b/src/api/admin/api_server.rs index 2f2e3284..82337b7e 100644 --- a/src/api/admin/api_server.rs +++ b/src/api/admin/api_server.rs @@ -39,7 +39,7 @@ pub struct AdminApiServer { admin_token: Option, } -enum Endpoint { +pub enum Endpoint { Old(router_v1::Endpoint), New(String), } @@ -159,7 +159,7 @@ impl ApiHandler for AdminApiServer { 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(), + AdminApiRequest::Metrics(_req) => self.handle_metrics(), req => { let res = req.handle(&self.garage).await?; json_ok_response(&res) diff --git a/src/api/admin/bucket.rs b/src/api/admin/bucket.rs index f9accba5..8e19b93e 100644 --- a/src/api/admin/bucket.rs +++ b/src/api/admin/bucket.rs @@ -457,7 +457,7 @@ impl EndpointHandler for GlobalAliasBucketRequest { type Response = GlobalAliasBucketResponse; async fn handle(self, garage: &Arc) -> Result { - let bucket_id = parse_bucket_id(&self.id)?; + let bucket_id = parse_bucket_id(&self.bucket_id)?; let helper = garage.locked_helper().await; @@ -476,7 +476,7 @@ impl EndpointHandler for GlobalUnaliasBucketRequest { type Response = GlobalUnaliasBucketResponse; async fn handle(self, garage: &Arc) -> Result { - let bucket_id = parse_bucket_id(&self.id)?; + let bucket_id = parse_bucket_id(&self.bucket_id)?; let helper = garage.locked_helper().await; @@ -495,7 +495,7 @@ impl EndpointHandler for LocalAliasBucketRequest { type Response = LocalAliasBucketResponse; async fn handle(self, garage: &Arc) -> Result { - let bucket_id = parse_bucket_id(&self.id)?; + let bucket_id = parse_bucket_id(&self.bucket_id)?; let helper = garage.locked_helper().await; @@ -514,7 +514,7 @@ impl EndpointHandler for LocalUnaliasBucketRequest { type Response = LocalUnaliasBucketResponse; async fn handle(self, garage: &Arc) -> Result { - let bucket_id = parse_bucket_id(&self.id)?; + let bucket_id = parse_bucket_id(&self.bucket_id)?; let helper = garage.locked_helper().await; diff --git a/src/api/admin/macros.rs b/src/api/admin/macros.rs index a12dc40b..d8c8f6dc 100644 --- a/src/api/admin/macros.rs +++ b/src/api/admin/macros.rs @@ -22,7 +22,7 @@ macro_rules! admin_endpoints { } impl AdminApiRequest { - fn name(&self) -> &'static str { + pub fn name(&self) -> &'static str { match self { $( Self::$special_endpoint(_) => stringify!($special_endpoint), diff --git a/src/api/admin/router_v2.rs b/src/api/admin/router_v2.rs index e0c54f0e..dacf6793 100644 --- a/src/api/admin/router_v2.rs +++ b/src/api/admin/router_v2.rs @@ -43,22 +43,22 @@ impl AdminApiRequest { POST UpdateKey (body_field, query::id), POST CreateKey (body), POST ImportKey (body), - DELETE DeleteKey (query::id), + POST 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), + POST DeleteBucket (query::id), + POST 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), + POST GlobalAliasBucket (body), + POST GlobalUnaliasBucket (body), + POST LocalAliasBucket (body), + POST LocalUnaliasBucket (body), ]); if let Some(message) = query.nonempty_message() { @@ -131,7 +131,11 @@ impl AdminApiRequest { let body = parse_json_body::(req).await?; Ok(AdminApiRequest::UpdateKey(UpdateKeyRequest { id, body })) } - Endpoint::DeleteKey { id } => Ok(AdminApiRequest::DeleteKey(DeleteKeyRequest { id })), + + // DeleteKey semantics changed: + // - in v1/ : HTTP DELETE => HTTP 204 No Content + // - in v2/ : HTTP POST => HTTP 200 Ok + // Endpoint::DeleteKey { id } => Ok(AdminApiRequest::DeleteKey(DeleteKeyRequest { id })), // Buckets Endpoint::ListBuckets => Ok(AdminApiRequest::ListBuckets(ListBucketsRequest)), @@ -145,9 +149,13 @@ impl AdminApiRequest { let req = parse_json_body::(req).await?; Ok(AdminApiRequest::CreateBucket(req)) } - Endpoint::DeleteBucket { id } => { - Ok(AdminApiRequest::DeleteBucket(DeleteBucketRequest { id })) - } + + // DeleteBucket semantics changed:: + // - in v1/ : HTTP DELETE => HTTP 204 No Content + // - in v2/ : HTTP POST => HTTP 200 Ok + // Endpoint::DeleteBucket { id } => { + // Ok(AdminApiRequest::DeleteBucket(DeleteBucketRequest { id })) + // } Endpoint::UpdateBucket { id } => { let body = parse_json_body::(req).await?; Ok(AdminApiRequest::UpdateBucket(UpdateBucketRequest { @@ -167,10 +175,16 @@ impl AdminApiRequest { } // Bucket aliasing Endpoint::GlobalAliasBucket { id, alias } => Ok(AdminApiRequest::GlobalAliasBucket( - GlobalAliasBucketRequest { id, alias }, + GlobalAliasBucketRequest { + bucket_id: id, + alias, + }, )), Endpoint::GlobalUnaliasBucket { id, alias } => Ok( - AdminApiRequest::GlobalUnaliasBucket(GlobalUnaliasBucketRequest { id, alias }), + AdminApiRequest::GlobalUnaliasBucket(GlobalUnaliasBucketRequest { + bucket_id: id, + alias, + }), ), Endpoint::LocalAliasBucket { id, @@ -178,7 +192,7 @@ impl AdminApiRequest { alias, } => Ok(AdminApiRequest::LocalAliasBucket(LocalAliasBucketRequest { access_key_id, - id, + bucket_id: id, alias, })), Endpoint::LocalUnaliasBucket { @@ -188,7 +202,7 @@ impl AdminApiRequest { } => Ok(AdminApiRequest::LocalUnaliasBucket( LocalUnaliasBucketRequest { access_key_id, - id, + bucket_id: id, alias, }, )), -- cgit v1.2.3 From f538dc34d3ad6f6c0d01d40f8f1f6b81458534db Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 28 Jan 2025 17:07:34 +0100 Subject: admin api: make all requests and responses (de)serializable --- src/api/admin/api.rs | 126 ++++++++++++++++++++++++++--------------------- src/api/admin/cluster.rs | 10 ++-- src/api/admin/macros.rs | 3 +- 3 files changed, 79 insertions(+), 60 deletions(-) (limited to 'src/api') diff --git a/src/api/admin/api.rs b/src/api/admin/api.rs index 457863e0..01b4f928 100644 --- a/src/api/admin/api.rs +++ b/src/api/admin/api.rs @@ -64,14 +64,18 @@ admin_endpoints![ // Special endpoints // ********************************************** +#[derive(Serialize, Deserialize)] pub struct OptionsRequest; +#[derive(Serialize, Deserialize)] pub struct CheckDomainRequest { pub domain: String, } +#[derive(Serialize, Deserialize)] pub struct HealthRequest; +#[derive(Serialize, Deserialize)] pub struct MetricsRequest; // ********************************************** @@ -80,21 +84,22 @@ pub struct MetricsRequest; // ---- GetClusterStatus ---- +#[derive(Serialize, Deserialize)] pub struct GetClusterStatusRequest; -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetClusterStatusResponse { pub node: String, - pub garage_version: &'static str, - pub garage_features: Option<&'static [&'static str]>, - pub rust_version: &'static str, + pub garage_version: String, + pub garage_features: Option>, + pub rust_version: String, pub db_engine: String, pub layout_version: u64, pub nodes: Vec, } -#[derive(Serialize, Default)] +#[derive(Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct NodeResp { pub id: String, @@ -110,7 +115,7 @@ pub struct NodeResp { pub metadata_partition: Option, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NodeRoleResp { pub id: String, @@ -119,7 +124,7 @@ pub struct NodeRoleResp { pub tags: Vec, } -#[derive(Serialize, Default)] +#[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct FreeSpaceResp { pub available: u64, @@ -128,12 +133,13 @@ pub struct FreeSpaceResp { // ---- GetClusterHealth ---- +#[derive(Serialize, Deserialize)] pub struct GetClusterHealthRequest; -#[derive(Debug, Clone, Copy, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetClusterHealthResponse { - pub status: &'static str, + pub status: String, pub known_nodes: usize, pub connected_nodes: usize, pub storage_nodes: usize, @@ -145,13 +151,13 @@ pub struct GetClusterHealthResponse { // ---- ConnectClusterNodes ---- -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConnectClusterNodesRequest(pub Vec); -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct ConnectClusterNodesResponse(pub Vec); -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ConnectClusterNodeResponse { pub success: bool, @@ -160,9 +166,10 @@ pub struct ConnectClusterNodeResponse { // ---- GetClusterLayout ---- +#[derive(Serialize, Deserialize)] pub struct GetClusterLayoutRequest; -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetClusterLayoutResponse { pub version: u64, @@ -193,21 +200,21 @@ pub enum NodeRoleChangeEnum { // ---- UpdateClusterLayout ---- -#[derive(Deserialize)] +#[derive(Serialize, Deserialize)] pub struct UpdateClusterLayoutRequest(pub Vec); -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct UpdateClusterLayoutResponse(pub GetClusterLayoutResponse); // ---- ApplyClusterLayout ---- -#[derive(Deserialize)] +#[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ApplyClusterLayoutRequest { pub version: u64, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ApplyClusterLayoutResponse { pub message: Vec, @@ -216,9 +223,10 @@ pub struct ApplyClusterLayoutResponse { // ---- RevertClusterLayout ---- +#[derive(Serialize, Deserialize)] pub struct RevertClusterLayoutRequest; -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct RevertClusterLayoutResponse(pub GetClusterLayoutResponse); // ********************************************** @@ -227,12 +235,13 @@ pub struct RevertClusterLayoutResponse(pub GetClusterLayoutResponse); // ---- ListKeys ---- +#[derive(Serialize, Deserialize)] pub struct ListKeysRequest; -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct ListKeysResponse(pub Vec); -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ListKeysResponseItem { pub id: String, @@ -241,13 +250,14 @@ pub struct ListKeysResponseItem { // ---- GetKeyInfo ---- +#[derive(Serialize, Deserialize)] pub struct GetKeyInfoRequest { pub id: Option, pub search: Option, pub show_secret_key: bool, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetKeyInfoResponse { pub name: String, @@ -265,7 +275,7 @@ pub struct KeyPerm { pub create_bucket: bool, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct KeyInfoBucketResponse { pub id: String, @@ -287,18 +297,18 @@ pub struct ApiBucketKeyPerm { // ---- CreateKey ---- -#[derive(Deserialize)] +#[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateKeyRequest { pub name: Option, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct CreateKeyResponse(pub GetKeyInfoResponse); // ---- ImportKey ---- -#[derive(Deserialize)] +#[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ImportKeyRequest { pub access_key_id: String, @@ -306,20 +316,21 @@ pub struct ImportKeyRequest { pub name: Option, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct ImportKeyResponse(pub GetKeyInfoResponse); // ---- UpdateKey ---- +#[derive(Serialize, Deserialize)] pub struct UpdateKeyRequest { pub id: String, pub body: UpdateKeyRequestBody, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct UpdateKeyResponse(pub GetKeyInfoResponse); -#[derive(Deserialize)] +#[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UpdateKeyRequestBody { // TODO: id (get parameter) goes here @@ -330,11 +341,12 @@ pub struct UpdateKeyRequestBody { // ---- DeleteKey ---- +#[derive(Serialize, Deserialize)] pub struct DeleteKeyRequest { pub id: String, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct DeleteKeyResponse; // ********************************************** @@ -343,12 +355,13 @@ pub struct DeleteKeyResponse; // ---- ListBuckets ---- +#[derive(Serialize, Deserialize)] pub struct ListBucketsRequest; -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct ListBucketsResponse(pub Vec); -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ListBucketsResponseItem { pub id: String, @@ -356,7 +369,7 @@ pub struct ListBucketsResponseItem { pub local_aliases: Vec, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct BucketLocalAlias { pub access_key_id: String, @@ -365,12 +378,13 @@ pub struct BucketLocalAlias { // ---- GetBucketInfo ---- +#[derive(Serialize, Deserialize)] pub struct GetBucketInfoRequest { pub id: Option, pub global_alias: Option, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetBucketInfoResponse { pub id: String, @@ -388,14 +402,14 @@ pub struct GetBucketInfoResponse { pub quotas: ApiBucketQuotas, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetBucketInfoWebsiteResponse { pub index_document: String, pub error_document: Option, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetBucketInfoKey { pub access_key_id: String, @@ -413,17 +427,17 @@ pub struct ApiBucketQuotas { // ---- CreateBucket ---- -#[derive(Deserialize)] +#[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateBucketRequest { pub global_alias: Option, pub local_alias: Option, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct CreateBucketResponse(pub GetBucketInfoResponse); -#[derive(Deserialize)] +#[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateBucketLocalAlias { pub access_key_id: String, @@ -434,22 +448,23 @@ pub struct CreateBucketLocalAlias { // ---- UpdateBucket ---- +#[derive(Serialize, Deserialize)] pub struct UpdateBucketRequest { pub id: String, pub body: UpdateBucketRequestBody, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct UpdateBucketResponse(pub GetBucketInfoResponse); -#[derive(Deserialize)] +#[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UpdateBucketRequestBody { pub website_access: Option, pub quotas: Option, } -#[derive(Deserialize)] +#[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UpdateBucketWebsiteAccess { pub enabled: bool, @@ -459,11 +474,12 @@ pub struct UpdateBucketWebsiteAccess { // ---- DeleteBucket ---- +#[derive(Serialize, Deserialize)] pub struct DeleteBucketRequest { pub id: String, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct DeleteBucketResponse; // ********************************************** @@ -472,13 +488,13 @@ pub struct DeleteBucketResponse; // ---- BucketAllowKey ---- -#[derive(Deserialize)] +#[derive(Serialize, Deserialize)] pub struct BucketAllowKeyRequest(pub BucketKeyPermChangeRequest); -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct BucketAllowKeyResponse(pub GetBucketInfoResponse); -#[derive(Deserialize)] +#[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct BucketKeyPermChangeRequest { pub bucket_id: String, @@ -488,10 +504,10 @@ pub struct BucketKeyPermChangeRequest { // ---- BucketDenyKey ---- -#[derive(Deserialize)] +#[derive(Serialize, Deserialize)] pub struct BucketDenyKeyRequest(pub BucketKeyPermChangeRequest); -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct BucketDenyKeyResponse(pub GetBucketInfoResponse); // ********************************************** @@ -500,46 +516,46 @@ pub struct BucketDenyKeyResponse(pub GetBucketInfoResponse); // ---- GlobalAliasBucket ---- -#[derive(Deserialize)] +#[derive(Serialize, Deserialize)] pub struct GlobalAliasBucketRequest { pub bucket_id: String, pub alias: String, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct GlobalAliasBucketResponse(pub GetBucketInfoResponse); // ---- GlobalUnaliasBucket ---- -#[derive(Deserialize)] +#[derive(Serialize, Deserialize)] pub struct GlobalUnaliasBucketRequest { pub bucket_id: String, pub alias: String, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct GlobalUnaliasBucketResponse(pub GetBucketInfoResponse); // ---- LocalAliasBucket ---- -#[derive(Deserialize)] +#[derive(Serialize, Deserialize)] pub struct LocalAliasBucketRequest { pub bucket_id: String, pub access_key_id: String, pub alias: String, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct LocalAliasBucketResponse(pub GetBucketInfoResponse); // ---- LocalUnaliasBucket ---- -#[derive(Deserialize)] +#[derive(Serialize, Deserialize)] pub struct LocalUnaliasBucketRequest { pub bucket_id: String, pub access_key_id: String, pub alias: String, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct LocalUnaliasBucketResponse(pub GetBucketInfoResponse); diff --git a/src/api/admin/cluster.rs b/src/api/admin/cluster.rs index c7eb7e7d..3327cb4c 100644 --- a/src/api/admin/cluster.rs +++ b/src/api/admin/cluster.rs @@ -112,9 +112,10 @@ impl EndpointHandler for GetClusterStatusRequest { Ok(GetClusterStatusResponse { node: hex::encode(garage.system.id), - garage_version: garage_util::version::garage_version(), - garage_features: garage_util::version::garage_features(), - rust_version: garage_util::version::rust_version(), + garage_version: garage_util::version::garage_version().to_string(), + garage_features: garage_util::version::garage_features() + .map(|features| features.iter().map(ToString::to_string).collect()), + rust_version: garage_util::version::rust_version().to_string(), db_engine: garage.db.engine(), layout_version: layout.current().version, nodes, @@ -134,7 +135,8 @@ impl EndpointHandler for GetClusterHealthRequest { ClusterHealthStatus::Healthy => "healthy", ClusterHealthStatus::Degraded => "degraded", ClusterHealthStatus::Unavailable => "unavailable", - }, + } + .to_string(), known_nodes: health.known_nodes, connected_nodes: health.connected_nodes, storage_nodes: health.storage_nodes, diff --git a/src/api/admin/macros.rs b/src/api/admin/macros.rs index d8c8f6dc..d68ba37f 100644 --- a/src/api/admin/macros.rs +++ b/src/api/admin/macros.rs @@ -4,6 +4,7 @@ macro_rules! admin_endpoints { $($endpoint:ident,)* ] => { paste! { + #[derive(Serialize, Deserialize)] pub enum AdminApiRequest { $( $special_endpoint( [<$special_endpoint Request>] ), @@ -13,7 +14,7 @@ macro_rules! admin_endpoints { )* } - #[derive(Serialize)] + #[derive(Serialize, Deserialize)] #[serde(untagged)] pub enum AdminApiResponse { $( -- cgit v1.2.3 From 4cb45bd398afd7966cec5d4dfa4dd325c114f93c Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 28 Jan 2025 18:15:36 +0100 Subject: admin api: fix CORS to work in browser --- src/api/admin/api_server.rs | 9 +++++++-- src/api/admin/router_v2.rs | 1 + src/api/admin/special.rs | 11 +++++++---- 3 files changed, 15 insertions(+), 6 deletions(-) (limited to 'src/api') diff --git a/src/api/admin/api_server.rs b/src/api/admin/api_server.rs index 82337b7e..92da3245 100644 --- a/src/api/admin/api_server.rs +++ b/src/api/admin/api_server.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use argon2::password_hash::PasswordHash; use async_trait::async_trait; -use http::header::AUTHORIZATION; +use http::header::{HeaderValue, ACCESS_CONTROL_ALLOW_ORIGIN, AUTHORIZATION}; use hyper::{body::Incoming as IncomingBody, Request, Response, StatusCode}; use tokio::sync::watch; @@ -134,6 +134,8 @@ impl ApiHandler for AdminApiServer { Endpoint::New(_) => AdminApiRequest::from_request(req).await?, }; + info!("Admin request: {}", request.name()); + let required_auth_hash = match request.authorization_type() { Authorization::None => None, @@ -162,7 +164,10 @@ impl ApiHandler for AdminApiServer { AdminApiRequest::Metrics(_req) => self.handle_metrics(), req => { let res = req.handle(&self.garage).await?; - json_ok_response(&res) + let mut res = json_ok_response(&res)?; + res.headers_mut() + .insert(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*")); + Ok(res) } } } diff --git a/src/api/admin/router_v2.rs b/src/api/admin/router_v2.rs index dacf6793..c7a5e316 100644 --- a/src/api/admin/router_v2.rs +++ b/src/api/admin/router_v2.rs @@ -219,6 +219,7 @@ impl AdminApiRequest { /// Get the kind of authorization which is required to perform the operation. pub fn authorization_type(&self) -> Authorization { match self { + Self::Options(_) => Authorization::None, Self::Health(_) => Authorization::None, Self::CheckDomain(_) => Authorization::None, Self::Metrics(_) => Authorization::MetricsToken, diff --git a/src/api/admin/special.rs b/src/api/admin/special.rs index 0239021a..da3764d9 100644 --- a/src/api/admin/special.rs +++ b/src/api/admin/special.rs @@ -2,7 +2,9 @@ use std::sync::Arc; use async_trait::async_trait; -use http::header::{ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ALLOW}; +use http::header::{ + ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ALLOW, +}; use hyper::{Response, StatusCode}; use garage_model::garage::Garage; @@ -20,9 +22,10 @@ impl EndpointHandler for OptionsRequest { async fn handle(self, _garage: &Arc) -> Result, Error> { Ok(Response::builder() - .status(StatusCode::NO_CONTENT) - .header(ALLOW, "OPTIONS, GET, POST") - .header(ACCESS_CONTROL_ALLOW_METHODS, "OPTIONS, GET, POST") + .status(StatusCode::OK) + .header(ALLOW, "OPTIONS,GET,POST") + .header(ACCESS_CONTROL_ALLOW_METHODS, "OPTIONS,GET,POST") + .header(ACCESS_CONTROL_ALLOW_HEADERS, "authorization,content-type") .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*") .body(empty_body())?) } -- cgit v1.2.3 From f8ed3fdbc4cd0211f7f7cff2871cfe98e621a9fe Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 28 Jan 2025 18:40:40 +0100 Subject: fix test_website_check_domain --- src/api/router_macros.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) (limited to 'src/api') diff --git a/src/api/router_macros.rs b/src/api/router_macros.rs index e8c99909..142cdc11 100644 --- a/src/api/router_macros.rs +++ b/src/api/router_macros.rs @@ -146,7 +146,10 @@ macro_rules! router_match { }}; (@@parse_param $query:expr, query, $param:ident) => {{ // extract mendatory query parameter - $query.$param.take().ok_or_bad_request("Missing argument for endpoint")?.into_owned() + $query.$param.take() + .ok_or_bad_request( + format!("Missing argument `{}` for endpoint", stringify!($param)) + )?.into_owned() }}; (@@parse_param $query:expr, opt_parse, $param:ident) => {{ // extract and parse optional query parameter @@ -160,7 +163,10 @@ macro_rules! router_match { (@@parse_param $query:expr, parse, $param:ident) => {{ // extract and parse mandatory query parameter // both missing and un-parseable parameters are reported as errors - $query.$param.take().ok_or_bad_request("Missing argument for endpoint")? + $query.$param.take() + .ok_or_bad_request( + format!("Missing argument `{}` for endpoint", stringify!($param)) + )? .parse() .map_err(|_| Error::bad_request("Failed to parse query parameter"))? }}; @@ -256,6 +262,7 @@ macro_rules! generateQueryParameters { }, )* $( + // FIXME: remove if !v.is_empty() ? $f_param => if !v.is_empty() { if res.$f_name.replace(v).is_some() { return Err(Error::bad_request(format!( -- cgit v1.2.3 From ba810b2e8157855df36b5f8dc9d5fced40efbafd Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 28 Jan 2025 18:51:15 +0100 Subject: admin api: rename bucket aliasing operations --- src/api/admin/api.rs | 32 ++++++++++++++++---------------- src/api/admin/bucket.rs | 40 ++++++++++++++++++++-------------------- src/api/admin/router_v2.rs | 20 ++++++++++---------- 3 files changed, 46 insertions(+), 46 deletions(-) (limited to 'src/api') diff --git a/src/api/admin/api.rs b/src/api/admin/api.rs index 01b4f928..632711d1 100644 --- a/src/api/admin/api.rs +++ b/src/api/admin/api.rs @@ -54,10 +54,10 @@ admin_endpoints![ BucketDenyKey, // Operations on bucket aliases - GlobalAliasBucket, - GlobalUnaliasBucket, - LocalAliasBucket, - LocalUnaliasBucket, + AddGlobalBucketAlias, + RemoveGlobalBucketAlias, + AddLocalBucketAlias, + RemoveLocalBucketAlias, ]; // ********************************************** @@ -514,48 +514,48 @@ pub struct BucketDenyKeyResponse(pub GetBucketInfoResponse); // Operations on bucket aliases // ********************************************** -// ---- GlobalAliasBucket ---- +// ---- AddGlobalBucketAlias ---- #[derive(Serialize, Deserialize)] -pub struct GlobalAliasBucketRequest { +pub struct AddGlobalBucketAliasRequest { pub bucket_id: String, pub alias: String, } #[derive(Serialize, Deserialize)] -pub struct GlobalAliasBucketResponse(pub GetBucketInfoResponse); +pub struct AddGlobalBucketAliasResponse(pub GetBucketInfoResponse); -// ---- GlobalUnaliasBucket ---- +// ---- RemoveGlobalBucketAlias ---- #[derive(Serialize, Deserialize)] -pub struct GlobalUnaliasBucketRequest { +pub struct RemoveGlobalBucketAliasRequest { pub bucket_id: String, pub alias: String, } #[derive(Serialize, Deserialize)] -pub struct GlobalUnaliasBucketResponse(pub GetBucketInfoResponse); +pub struct RemoveGlobalBucketAliasResponse(pub GetBucketInfoResponse); -// ---- LocalAliasBucket ---- +// ---- AddLocalBucketAlias ---- #[derive(Serialize, Deserialize)] -pub struct LocalAliasBucketRequest { +pub struct AddLocalBucketAliasRequest { pub bucket_id: String, pub access_key_id: String, pub alias: String, } #[derive(Serialize, Deserialize)] -pub struct LocalAliasBucketResponse(pub GetBucketInfoResponse); +pub struct AddLocalBucketAliasResponse(pub GetBucketInfoResponse); -// ---- LocalUnaliasBucket ---- +// ---- RemoveLocalBucketAlias ---- #[derive(Serialize, Deserialize)] -pub struct LocalUnaliasBucketRequest { +pub struct RemoveLocalBucketAliasRequest { pub bucket_id: String, pub access_key_id: String, pub alias: String, } #[derive(Serialize, Deserialize)] -pub struct LocalUnaliasBucketResponse(pub GetBucketInfoResponse); +pub struct RemoveLocalBucketAliasResponse(pub GetBucketInfoResponse); diff --git a/src/api/admin/bucket.rs b/src/api/admin/bucket.rs index 8e19b93e..09952bff 100644 --- a/src/api/admin/bucket.rs +++ b/src/api/admin/bucket.rs @@ -22,10 +22,10 @@ use crate::admin::api::{ BucketDenyKeyResponse, BucketKeyPermChangeRequest, BucketLocalAlias, CreateBucketRequest, CreateBucketResponse, DeleteBucketRequest, DeleteBucketResponse, GetBucketInfoKey, GetBucketInfoRequest, GetBucketInfoResponse, GetBucketInfoWebsiteResponse, - GlobalAliasBucketRequest, GlobalAliasBucketResponse, GlobalUnaliasBucketRequest, - GlobalUnaliasBucketResponse, ListBucketsRequest, ListBucketsResponse, ListBucketsResponseItem, - LocalAliasBucketRequest, LocalAliasBucketResponse, LocalUnaliasBucketRequest, - LocalUnaliasBucketResponse, UpdateBucketRequest, UpdateBucketResponse, + AddGlobalBucketAliasRequest, AddGlobalBucketAliasResponse, RemoveGlobalBucketAliasRequest, + RemoveGlobalBucketAliasResponse, ListBucketsRequest, ListBucketsResponse, ListBucketsResponseItem, + AddLocalBucketAliasRequest, AddLocalBucketAliasResponse, RemoveLocalBucketAliasRequest, + RemoveLocalBucketAliasResponse, UpdateBucketRequest, UpdateBucketResponse, }; use crate::admin::error::*; use crate::admin::EndpointHandler; @@ -453,10 +453,10 @@ pub async fn handle_bucket_change_key_perm( // ---- BUCKET ALIASES ---- #[async_trait] -impl EndpointHandler for GlobalAliasBucketRequest { - type Response = GlobalAliasBucketResponse; +impl EndpointHandler for AddGlobalBucketAliasRequest { + type Response = AddGlobalBucketAliasResponse; - async fn handle(self, garage: &Arc) -> Result { + async fn handle(self, garage: &Arc) -> Result { let bucket_id = parse_bucket_id(&self.bucket_id)?; let helper = garage.locked_helper().await; @@ -465,17 +465,17 @@ impl EndpointHandler for GlobalAliasBucketRequest { .set_global_bucket_alias(bucket_id, &self.alias) .await?; - Ok(GlobalAliasBucketResponse( + Ok(AddGlobalBucketAliasResponse( bucket_info_results(garage, bucket_id).await?, )) } } #[async_trait] -impl EndpointHandler for GlobalUnaliasBucketRequest { - type Response = GlobalUnaliasBucketResponse; +impl EndpointHandler for RemoveGlobalBucketAliasRequest { + type Response = RemoveGlobalBucketAliasResponse; - async fn handle(self, garage: &Arc) -> Result { + async fn handle(self, garage: &Arc) -> Result { let bucket_id = parse_bucket_id(&self.bucket_id)?; let helper = garage.locked_helper().await; @@ -484,17 +484,17 @@ impl EndpointHandler for GlobalUnaliasBucketRequest { .unset_global_bucket_alias(bucket_id, &self.alias) .await?; - Ok(GlobalUnaliasBucketResponse( + Ok(RemoveGlobalBucketAliasResponse( bucket_info_results(garage, bucket_id).await?, )) } } #[async_trait] -impl EndpointHandler for LocalAliasBucketRequest { - type Response = LocalAliasBucketResponse; +impl EndpointHandler for AddLocalBucketAliasRequest { + type Response = AddLocalBucketAliasResponse; - async fn handle(self, garage: &Arc) -> Result { + async fn handle(self, garage: &Arc) -> Result { let bucket_id = parse_bucket_id(&self.bucket_id)?; let helper = garage.locked_helper().await; @@ -503,17 +503,17 @@ impl EndpointHandler for LocalAliasBucketRequest { .set_local_bucket_alias(bucket_id, &self.access_key_id, &self.alias) .await?; - Ok(LocalAliasBucketResponse( + Ok(AddLocalBucketAliasResponse( bucket_info_results(garage, bucket_id).await?, )) } } #[async_trait] -impl EndpointHandler for LocalUnaliasBucketRequest { - type Response = LocalUnaliasBucketResponse; +impl EndpointHandler for RemoveLocalBucketAliasRequest { + type Response = RemoveLocalBucketAliasResponse; - async fn handle(self, garage: &Arc) -> Result { + async fn handle(self, garage: &Arc) -> Result { let bucket_id = parse_bucket_id(&self.bucket_id)?; let helper = garage.locked_helper().await; @@ -522,7 +522,7 @@ impl EndpointHandler for LocalUnaliasBucketRequest { .unset_local_bucket_alias(bucket_id, &self.access_key_id, &self.alias) .await?; - Ok(LocalUnaliasBucketResponse( + Ok(RemoveLocalBucketAliasResponse( bucket_info_results(garage, bucket_id).await?, )) } diff --git a/src/api/admin/router_v2.rs b/src/api/admin/router_v2.rs index c7a5e316..6faa2ab1 100644 --- a/src/api/admin/router_v2.rs +++ b/src/api/admin/router_v2.rs @@ -55,10 +55,10 @@ impl AdminApiRequest { POST BucketAllowKey (body), POST BucketDenyKey (body), // Bucket aliases - POST GlobalAliasBucket (body), - POST GlobalUnaliasBucket (body), - POST LocalAliasBucket (body), - POST LocalUnaliasBucket (body), + POST AddGlobalBucketAlias (body), + POST RemoveGlobalBucketAlias (body), + POST AddLocalBucketAlias (body), + POST RemoveLocalBucketAlias (body), ]); if let Some(message) = query.nonempty_message() { @@ -174,14 +174,14 @@ impl AdminApiRequest { Ok(AdminApiRequest::BucketDenyKey(BucketDenyKeyRequest(req))) } // Bucket aliasing - Endpoint::GlobalAliasBucket { id, alias } => Ok(AdminApiRequest::GlobalAliasBucket( - GlobalAliasBucketRequest { + Endpoint::GlobalAliasBucket { id, alias } => Ok(AdminApiRequest::AddGlobalBucketAlias( + AddGlobalBucketAliasRequest { bucket_id: id, alias, }, )), Endpoint::GlobalUnaliasBucket { id, alias } => Ok( - AdminApiRequest::GlobalUnaliasBucket(GlobalUnaliasBucketRequest { + AdminApiRequest::RemoveGlobalBucketAlias(RemoveGlobalBucketAliasRequest { bucket_id: id, alias, }), @@ -190,7 +190,7 @@ impl AdminApiRequest { id, access_key_id, alias, - } => Ok(AdminApiRequest::LocalAliasBucket(LocalAliasBucketRequest { + } => Ok(AdminApiRequest::AddLocalBucketAlias(AddLocalBucketAliasRequest { access_key_id, bucket_id: id, alias, @@ -199,8 +199,8 @@ impl AdminApiRequest { id, access_key_id, alias, - } => Ok(AdminApiRequest::LocalUnaliasBucket( - LocalUnaliasBucketRequest { + } => Ok(AdminApiRequest::RemoveLocalBucketAlias( + RemoveLocalBucketAliasRequest { access_key_id, bucket_id: id, alias, -- cgit v1.2.3 From 5fefbd94e9f8cded0d911f7cdae3d0382762607c Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 28 Jan 2025 18:53:44 +0100 Subject: admin api: rename allow/deny api calls in api v2 --- src/api/admin/api.rs | 16 ++++++++-------- src/api/admin/bucket.rs | 20 ++++++++++---------- src/api/admin/router_v2.rs | 8 ++++---- 3 files changed, 22 insertions(+), 22 deletions(-) (limited to 'src/api') diff --git a/src/api/admin/api.rs b/src/api/admin/api.rs index 632711d1..c3559587 100644 --- a/src/api/admin/api.rs +++ b/src/api/admin/api.rs @@ -50,8 +50,8 @@ admin_endpoints![ DeleteBucket, // Operations on permissions for keys on buckets - BucketAllowKey, - BucketDenyKey, + AllowBucketKey, + DenyBucketKey, // Operations on bucket aliases AddGlobalBucketAlias, @@ -486,13 +486,13 @@ pub struct DeleteBucketResponse; // Operations on permissions for keys on buckets // ********************************************** -// ---- BucketAllowKey ---- +// ---- AllowBucketKey ---- #[derive(Serialize, Deserialize)] -pub struct BucketAllowKeyRequest(pub BucketKeyPermChangeRequest); +pub struct AllowBucketKeyRequest(pub BucketKeyPermChangeRequest); #[derive(Serialize, Deserialize)] -pub struct BucketAllowKeyResponse(pub GetBucketInfoResponse); +pub struct AllowBucketKeyResponse(pub GetBucketInfoResponse); #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -502,13 +502,13 @@ pub struct BucketKeyPermChangeRequest { pub permissions: ApiBucketKeyPerm, } -// ---- BucketDenyKey ---- +// ---- DenyBucketKey ---- #[derive(Serialize, Deserialize)] -pub struct BucketDenyKeyRequest(pub BucketKeyPermChangeRequest); +pub struct DenyBucketKeyRequest(pub BucketKeyPermChangeRequest); #[derive(Serialize, Deserialize)] -pub struct BucketDenyKeyResponse(pub GetBucketInfoResponse); +pub struct DenyBucketKeyResponse(pub GetBucketInfoResponse); // ********************************************** // Operations on bucket aliases diff --git a/src/api/admin/bucket.rs b/src/api/admin/bucket.rs index 09952bff..885c1749 100644 --- a/src/api/admin/bucket.rs +++ b/src/api/admin/bucket.rs @@ -18,8 +18,8 @@ use garage_model::s3::object_table::*; use crate::admin::api::ApiBucketKeyPerm; use crate::admin::api::{ - ApiBucketQuotas, BucketAllowKeyRequest, BucketAllowKeyResponse, BucketDenyKeyRequest, - BucketDenyKeyResponse, BucketKeyPermChangeRequest, BucketLocalAlias, CreateBucketRequest, + ApiBucketQuotas, AllowBucketKeyRequest, AllowBucketKeyResponse, DenyBucketKeyRequest, + DenyBucketKeyResponse, BucketKeyPermChangeRequest, BucketLocalAlias, CreateBucketRequest, CreateBucketResponse, DeleteBucketRequest, DeleteBucketResponse, GetBucketInfoKey, GetBucketInfoRequest, GetBucketInfoResponse, GetBucketInfoWebsiteResponse, AddGlobalBucketAliasRequest, AddGlobalBucketAliasResponse, RemoveGlobalBucketAliasRequest, @@ -394,22 +394,22 @@ impl EndpointHandler for UpdateBucketRequest { // ---- BUCKET/KEY PERMISSIONS ---- #[async_trait] -impl EndpointHandler for BucketAllowKeyRequest { - type Response = BucketAllowKeyResponse; +impl EndpointHandler for AllowBucketKeyRequest { + type Response = AllowBucketKeyResponse; - async fn handle(self, garage: &Arc) -> Result { + async fn handle(self, garage: &Arc) -> Result { let res = handle_bucket_change_key_perm(garage, self.0, true).await?; - Ok(BucketAllowKeyResponse(res)) + Ok(AllowBucketKeyResponse(res)) } } #[async_trait] -impl EndpointHandler for BucketDenyKeyRequest { - type Response = BucketDenyKeyResponse; +impl EndpointHandler for DenyBucketKeyRequest { + type Response = DenyBucketKeyResponse; - async fn handle(self, garage: &Arc) -> Result { + async fn handle(self, garage: &Arc) -> Result { let res = handle_bucket_change_key_perm(garage, self.0, false).await?; - Ok(BucketDenyKeyResponse(res)) + Ok(DenyBucketKeyResponse(res)) } } diff --git a/src/api/admin/router_v2.rs b/src/api/admin/router_v2.rs index 6faa2ab1..45613ea4 100644 --- a/src/api/admin/router_v2.rs +++ b/src/api/admin/router_v2.rs @@ -52,8 +52,8 @@ impl AdminApiRequest { POST DeleteBucket (query::id), POST UpdateBucket (body_field, query::id), // Bucket-key permissions - POST BucketAllowKey (body), - POST BucketDenyKey (body), + POST AllowBucketKey (body), + POST DenyBucketKey (body), // Bucket aliases POST AddGlobalBucketAlias (body), POST RemoveGlobalBucketAlias (body), @@ -167,11 +167,11 @@ impl AdminApiRequest { // Bucket-key permissions Endpoint::BucketAllowKey => { let req = parse_json_body::(req).await?; - Ok(AdminApiRequest::BucketAllowKey(BucketAllowKeyRequest(req))) + Ok(AdminApiRequest::AllowBucketKey(AllowBucketKeyRequest(req))) } Endpoint::BucketDenyKey => { let req = parse_json_body::(req).await?; - Ok(AdminApiRequest::BucketDenyKey(BucketDenyKeyRequest(req))) + Ok(AdminApiRequest::DenyBucketKey(DenyBucketKeyRequest(req))) } // Bucket aliasing Endpoint::GlobalAliasBucket { id, alias } => Ok(AdminApiRequest::AddGlobalBucketAlias( -- cgit v1.2.3 From 12ea4cda5fe033fc2b9f1fec51ddc3d8b860a85f Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 28 Jan 2025 19:03:39 +0100 Subject: admin api: merge calls to manage global/local aliases --- src/api/admin/api.rs | 44 +++++---------------- src/api/admin/bucket.rs | 98 ++++++++++++++++++---------------------------- src/api/admin/router_v2.rs | 34 ++++++++-------- 3 files changed, 65 insertions(+), 111 deletions(-) (limited to 'src/api') diff --git a/src/api/admin/api.rs b/src/api/admin/api.rs index c3559587..5fedd11f 100644 --- a/src/api/admin/api.rs +++ b/src/api/admin/api.rs @@ -54,10 +54,8 @@ admin_endpoints![ DenyBucketKey, // Operations on bucket aliases - AddGlobalBucketAlias, - RemoveGlobalBucketAlias, - AddLocalBucketAlias, - RemoveLocalBucketAlias, + AddBucketAlias, + RemoveBucketAlias, ]; // ********************************************** @@ -514,48 +512,26 @@ pub struct DenyBucketKeyResponse(pub GetBucketInfoResponse); // Operations on bucket aliases // ********************************************** -// ---- AddGlobalBucketAlias ---- +// ---- AddBucketAlias ---- #[derive(Serialize, Deserialize)] -pub struct AddGlobalBucketAliasRequest { +pub struct AddBucketAliasRequest { pub bucket_id: String, + pub access_key_id: Option, pub alias: String, } #[derive(Serialize, Deserialize)] -pub struct AddGlobalBucketAliasResponse(pub GetBucketInfoResponse); +pub struct AddBucketAliasResponse(pub GetBucketInfoResponse); -// ---- RemoveGlobalBucketAlias ---- +// ---- RemoveBucketAlias ---- #[derive(Serialize, Deserialize)] -pub struct RemoveGlobalBucketAliasRequest { +pub struct RemoveBucketAliasRequest { pub bucket_id: String, + pub access_key_id: Option, pub alias: String, } #[derive(Serialize, Deserialize)] -pub struct RemoveGlobalBucketAliasResponse(pub GetBucketInfoResponse); - -// ---- AddLocalBucketAlias ---- - -#[derive(Serialize, Deserialize)] -pub struct AddLocalBucketAliasRequest { - pub bucket_id: String, - pub access_key_id: String, - pub alias: String, -} - -#[derive(Serialize, Deserialize)] -pub struct AddLocalBucketAliasResponse(pub GetBucketInfoResponse); - -// ---- RemoveLocalBucketAlias ---- - -#[derive(Serialize, Deserialize)] -pub struct RemoveLocalBucketAliasRequest { - pub bucket_id: String, - pub access_key_id: String, - pub alias: String, -} - -#[derive(Serialize, Deserialize)] -pub struct RemoveLocalBucketAliasResponse(pub GetBucketInfoResponse); +pub struct RemoveBucketAliasResponse(pub GetBucketInfoResponse); diff --git a/src/api/admin/bucket.rs b/src/api/admin/bucket.rs index 885c1749..ee7a5e12 100644 --- a/src/api/admin/bucket.rs +++ b/src/api/admin/bucket.rs @@ -18,14 +18,12 @@ use garage_model::s3::object_table::*; use crate::admin::api::ApiBucketKeyPerm; use crate::admin::api::{ - ApiBucketQuotas, AllowBucketKeyRequest, AllowBucketKeyResponse, DenyBucketKeyRequest, - DenyBucketKeyResponse, BucketKeyPermChangeRequest, BucketLocalAlias, CreateBucketRequest, - CreateBucketResponse, DeleteBucketRequest, DeleteBucketResponse, GetBucketInfoKey, - GetBucketInfoRequest, GetBucketInfoResponse, GetBucketInfoWebsiteResponse, - AddGlobalBucketAliasRequest, AddGlobalBucketAliasResponse, RemoveGlobalBucketAliasRequest, - RemoveGlobalBucketAliasResponse, ListBucketsRequest, ListBucketsResponse, ListBucketsResponseItem, - AddLocalBucketAliasRequest, AddLocalBucketAliasResponse, RemoveLocalBucketAliasRequest, - RemoveLocalBucketAliasResponse, UpdateBucketRequest, UpdateBucketResponse, + AddBucketAliasRequest, AddBucketAliasResponse, AllowBucketKeyRequest, AllowBucketKeyResponse, + ApiBucketQuotas, BucketKeyPermChangeRequest, BucketLocalAlias, CreateBucketRequest, + CreateBucketResponse, DeleteBucketRequest, DeleteBucketResponse, DenyBucketKeyRequest, + DenyBucketKeyResponse, GetBucketInfoKey, GetBucketInfoRequest, GetBucketInfoResponse, + GetBucketInfoWebsiteResponse, ListBucketsRequest, ListBucketsResponse, ListBucketsResponseItem, + RemoveBucketAliasRequest, RemoveBucketAliasResponse, UpdateBucketRequest, UpdateBucketResponse, }; use crate::admin::error::*; use crate::admin::EndpointHandler; @@ -453,76 +451,56 @@ pub async fn handle_bucket_change_key_perm( // ---- BUCKET ALIASES ---- #[async_trait] -impl EndpointHandler for AddGlobalBucketAliasRequest { - type Response = AddGlobalBucketAliasResponse; +impl EndpointHandler for AddBucketAliasRequest { + type Response = AddBucketAliasResponse; - async fn handle(self, garage: &Arc) -> Result { + async fn handle(self, garage: &Arc) -> Result { let bucket_id = parse_bucket_id(&self.bucket_id)?; let helper = garage.locked_helper().await; - helper - .set_global_bucket_alias(bucket_id, &self.alias) - .await?; - - Ok(AddGlobalBucketAliasResponse( - bucket_info_results(garage, bucket_id).await?, - )) - } -} - -#[async_trait] -impl EndpointHandler for RemoveGlobalBucketAliasRequest { - type Response = RemoveGlobalBucketAliasResponse; - - async fn handle(self, garage: &Arc) -> Result { - let bucket_id = parse_bucket_id(&self.bucket_id)?; - - let helper = garage.locked_helper().await; - - helper - .unset_global_bucket_alias(bucket_id, &self.alias) - .await?; - - Ok(RemoveGlobalBucketAliasResponse( - bucket_info_results(garage, bucket_id).await?, - )) - } -} - -#[async_trait] -impl EndpointHandler for AddLocalBucketAliasRequest { - type Response = AddLocalBucketAliasResponse; - - async fn handle(self, garage: &Arc) -> Result { - let bucket_id = parse_bucket_id(&self.bucket_id)?; - - let helper = garage.locked_helper().await; - - helper - .set_local_bucket_alias(bucket_id, &self.access_key_id, &self.alias) - .await?; + match self.access_key_id { + None => { + helper + .set_global_bucket_alias(bucket_id, &self.alias) + .await?; + } + Some(ak) => { + helper + .set_local_bucket_alias(bucket_id, &ak, &self.alias) + .await?; + } + } - Ok(AddLocalBucketAliasResponse( + Ok(AddBucketAliasResponse( bucket_info_results(garage, bucket_id).await?, )) } } #[async_trait] -impl EndpointHandler for RemoveLocalBucketAliasRequest { - type Response = RemoveLocalBucketAliasResponse; +impl EndpointHandler for RemoveBucketAliasRequest { + type Response = RemoveBucketAliasResponse; - async fn handle(self, garage: &Arc) -> Result { + async fn handle(self, garage: &Arc) -> Result { let bucket_id = parse_bucket_id(&self.bucket_id)?; let helper = garage.locked_helper().await; - helper - .unset_local_bucket_alias(bucket_id, &self.access_key_id, &self.alias) - .await?; + match self.access_key_id { + None => { + helper + .unset_global_bucket_alias(bucket_id, &self.alias) + .await?; + } + Some(ak) => { + helper + .unset_local_bucket_alias(bucket_id, &ak, &self.alias) + .await?; + } + } - Ok(RemoveLocalBucketAliasResponse( + Ok(RemoveBucketAliasResponse( bucket_info_results(garage, bucket_id).await?, )) } diff --git a/src/api/admin/router_v2.rs b/src/api/admin/router_v2.rs index 45613ea4..a6f110a7 100644 --- a/src/api/admin/router_v2.rs +++ b/src/api/admin/router_v2.rs @@ -55,10 +55,8 @@ impl AdminApiRequest { POST AllowBucketKey (body), POST DenyBucketKey (body), // Bucket aliases - POST AddGlobalBucketAlias (body), - POST RemoveGlobalBucketAlias (body), - POST AddLocalBucketAlias (body), - POST RemoveLocalBucketAlias (body), + POST AddBucketAlias (body), + POST RemoveBucketAlias (body), ]); if let Some(message) = query.nonempty_message() { @@ -174,24 +172,26 @@ impl AdminApiRequest { Ok(AdminApiRequest::DenyBucketKey(DenyBucketKeyRequest(req))) } // Bucket aliasing - Endpoint::GlobalAliasBucket { id, alias } => Ok(AdminApiRequest::AddGlobalBucketAlias( - AddGlobalBucketAliasRequest { + Endpoint::GlobalAliasBucket { id, alias } => { + Ok(AdminApiRequest::AddBucketAlias(AddBucketAliasRequest { + access_key_id: None, bucket_id: id, alias, - }, - )), - Endpoint::GlobalUnaliasBucket { id, alias } => Ok( - AdminApiRequest::RemoveGlobalBucketAlias(RemoveGlobalBucketAliasRequest { + })) + } + Endpoint::GlobalUnaliasBucket { id, alias } => Ok(AdminApiRequest::RemoveBucketAlias( + RemoveBucketAliasRequest { + access_key_id: None, bucket_id: id, alias, - }), - ), + }, + )), Endpoint::LocalAliasBucket { id, access_key_id, alias, - } => Ok(AdminApiRequest::AddLocalBucketAlias(AddLocalBucketAliasRequest { - access_key_id, + } => Ok(AdminApiRequest::AddBucketAlias(AddBucketAliasRequest { + access_key_id: Some(access_key_id), bucket_id: id, alias, })), @@ -199,9 +199,9 @@ impl AdminApiRequest { id, access_key_id, alias, - } => Ok(AdminApiRequest::RemoveLocalBucketAlias( - RemoveLocalBucketAliasRequest { - access_key_id, + } => Ok(AdminApiRequest::RemoveBucketAlias( + RemoveBucketAliasRequest { + access_key_id: Some(access_key_id), bucket_id: id, alias, }, -- cgit v1.2.3 From 420bbc162dffd1246544168cf2e935efc60c5c98 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 29 Jan 2025 11:06:45 +0100 Subject: admin api: clearer syntax for AddBucketAlias and RemoveBucketAlias --- src/api/admin/api.rs | 22 ++++++++++++++++++---- src/api/admin/bucket.rs | 36 +++++++++++++++++------------------- src/api/admin/cluster.rs | 9 +-------- src/api/admin/key.rs | 7 +------ src/api/admin/router_v2.rs | 22 ++++++++++++++-------- 5 files changed, 51 insertions(+), 45 deletions(-) (limited to 'src/api') diff --git a/src/api/admin/api.rs b/src/api/admin/api.rs index 5fedd11f..eac93b6e 100644 --- a/src/api/admin/api.rs +++ b/src/api/admin/api.rs @@ -515,22 +515,36 @@ pub struct DenyBucketKeyResponse(pub GetBucketInfoResponse); // ---- AddBucketAlias ---- #[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct AddBucketAliasRequest { pub bucket_id: String, - pub access_key_id: Option, - pub alias: String, + #[serde(flatten)] + pub alias: BucketAliasEnum, } #[derive(Serialize, Deserialize)] pub struct AddBucketAliasResponse(pub GetBucketInfoResponse); +#[derive(Serialize, Deserialize)] +#[serde(untagged)] +pub enum BucketAliasEnum { + #[serde(rename_all = "camelCase")] + Global { global_alias: String }, + #[serde(rename_all = "camelCase")] + Local { + local_alias: String, + access_key_id: String, + }, +} + // ---- RemoveBucketAlias ---- #[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct RemoveBucketAliasRequest { pub bucket_id: String, - pub access_key_id: Option, - pub alias: String, + #[serde(flatten)] + pub alias: BucketAliasEnum, } #[derive(Serialize, Deserialize)] diff --git a/src/api/admin/bucket.rs b/src/api/admin/bucket.rs index ee7a5e12..0cc420ec 100644 --- a/src/api/admin/bucket.rs +++ b/src/api/admin/bucket.rs @@ -16,15 +16,7 @@ use garage_model::permission::*; use garage_model::s3::mpu_table; use garage_model::s3::object_table::*; -use crate::admin::api::ApiBucketKeyPerm; -use crate::admin::api::{ - AddBucketAliasRequest, AddBucketAliasResponse, AllowBucketKeyRequest, AllowBucketKeyResponse, - ApiBucketQuotas, BucketKeyPermChangeRequest, BucketLocalAlias, CreateBucketRequest, - CreateBucketResponse, DeleteBucketRequest, DeleteBucketResponse, DenyBucketKeyRequest, - DenyBucketKeyResponse, GetBucketInfoKey, GetBucketInfoRequest, GetBucketInfoResponse, - GetBucketInfoWebsiteResponse, ListBucketsRequest, ListBucketsResponse, ListBucketsResponseItem, - RemoveBucketAliasRequest, RemoveBucketAliasResponse, UpdateBucketRequest, UpdateBucketResponse, -}; +use crate::admin::api::*; use crate::admin::error::*; use crate::admin::EndpointHandler; use crate::common_error::CommonError; @@ -459,15 +451,18 @@ impl EndpointHandler for AddBucketAliasRequest { let helper = garage.locked_helper().await; - match self.access_key_id { - None => { + match self.alias { + BucketAliasEnum::Global { global_alias } => { helper - .set_global_bucket_alias(bucket_id, &self.alias) + .set_global_bucket_alias(bucket_id, &global_alias) .await?; } - Some(ak) => { + BucketAliasEnum::Local { + local_alias, + access_key_id, + } => { helper - .set_local_bucket_alias(bucket_id, &ak, &self.alias) + .set_local_bucket_alias(bucket_id, &access_key_id, &local_alias) .await?; } } @@ -487,15 +482,18 @@ impl EndpointHandler for RemoveBucketAliasRequest { let helper = garage.locked_helper().await; - match self.access_key_id { - None => { + match self.alias { + BucketAliasEnum::Global { global_alias } => { helper - .unset_global_bucket_alias(bucket_id, &self.alias) + .unset_global_bucket_alias(bucket_id, &global_alias) .await?; } - Some(ak) => { + BucketAliasEnum::Local { + local_alias, + access_key_id, + } => { helper - .unset_local_bucket_alias(bucket_id, &ak, &self.alias) + .unset_local_bucket_alias(bucket_id, &access_key_id, &local_alias) .await?; } } diff --git a/src/api/admin/cluster.rs b/src/api/admin/cluster.rs index 3327cb4c..112cb542 100644 --- a/src/api/admin/cluster.rs +++ b/src/api/admin/cluster.rs @@ -10,14 +10,7 @@ use garage_rpc::layout; use garage_model::garage::Garage; -use crate::admin::api::{ - ApplyClusterLayoutRequest, ApplyClusterLayoutResponse, ConnectClusterNodeResponse, - ConnectClusterNodesRequest, ConnectClusterNodesResponse, FreeSpaceResp, - GetClusterHealthRequest, GetClusterHealthResponse, GetClusterLayoutRequest, - GetClusterLayoutResponse, GetClusterStatusRequest, GetClusterStatusResponse, NodeResp, - NodeRoleChange, NodeRoleChangeEnum, NodeRoleResp, RevertClusterLayoutRequest, - RevertClusterLayoutResponse, UpdateClusterLayoutRequest, UpdateClusterLayoutResponse, -}; +use crate::admin::api::*; use crate::admin::error::*; use crate::admin::EndpointHandler; diff --git a/src/api/admin/key.rs b/src/api/admin/key.rs index 5bec2202..3e4201d9 100644 --- a/src/api/admin/key.rs +++ b/src/api/admin/key.rs @@ -8,12 +8,7 @@ use garage_table::*; use garage_model::garage::Garage; use garage_model::key_table::*; -use crate::admin::api::{ - ApiBucketKeyPerm, CreateKeyRequest, CreateKeyResponse, DeleteKeyRequest, DeleteKeyResponse, - GetKeyInfoRequest, GetKeyInfoResponse, ImportKeyRequest, ImportKeyResponse, - KeyInfoBucketResponse, KeyPerm, ListKeysRequest, ListKeysResponse, ListKeysResponseItem, - UpdateKeyRequest, UpdateKeyResponse, -}; +use crate::admin::api::*; use crate::admin::error::*; use crate::admin::EndpointHandler; diff --git a/src/api/admin/router_v2.rs b/src/api/admin/router_v2.rs index a6f110a7..29250f39 100644 --- a/src/api/admin/router_v2.rs +++ b/src/api/admin/router_v2.rs @@ -174,16 +174,18 @@ impl AdminApiRequest { // Bucket aliasing Endpoint::GlobalAliasBucket { id, alias } => { Ok(AdminApiRequest::AddBucketAlias(AddBucketAliasRequest { - access_key_id: None, bucket_id: id, - alias, + alias: BucketAliasEnum::Global { + global_alias: alias, + }, })) } Endpoint::GlobalUnaliasBucket { id, alias } => Ok(AdminApiRequest::RemoveBucketAlias( RemoveBucketAliasRequest { - access_key_id: None, bucket_id: id, - alias, + alias: BucketAliasEnum::Global { + global_alias: alias, + }, }, )), Endpoint::LocalAliasBucket { @@ -191,9 +193,11 @@ impl AdminApiRequest { access_key_id, alias, } => Ok(AdminApiRequest::AddBucketAlias(AddBucketAliasRequest { - access_key_id: Some(access_key_id), bucket_id: id, - alias, + alias: BucketAliasEnum::Local { + local_alias: alias, + access_key_id, + }, })), Endpoint::LocalUnaliasBucket { id, @@ -201,9 +205,11 @@ impl AdminApiRequest { alias, } => Ok(AdminApiRequest::RemoveBucketAlias( RemoveBucketAliasRequest { - access_key_id: Some(access_key_id), bucket_id: id, - alias, + alias: BucketAliasEnum::Local { + local_alias: alias, + access_key_id, + }, }, )), -- cgit v1.2.3 From 4f0b923c4f2bc9be80bf1e7ca61cc66c354cc7e0 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 29 Jan 2025 12:06:58 +0100 Subject: admin api: small fixes --- src/api/admin/api.rs | 22 ++++++++++++++++++---- src/api/admin/api_server.rs | 2 +- src/api/admin/cluster.rs | 4 ++-- src/api/admin/macros.rs | 19 ++++++++++++++++++- 4 files changed, 39 insertions(+), 8 deletions(-) (limited to 'src/api') diff --git a/src/api/admin/api.rs b/src/api/admin/api.rs index eac93b6e..39e05d51 100644 --- a/src/api/admin/api.rs +++ b/src/api/admin/api.rs @@ -13,9 +13,21 @@ use crate::admin::EndpointHandler; use crate::helpers::is_default; // 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 +// +// - An enum AdminApiResponse that contains a variant for all non-special endpoints. +// This enum is serialized in api_server.rs, without the enum tag, +// which gives directly the JSON response corresponding to the API call. +// This enum does not implement Deserialize as its meaning can be ambiguous. +// +// - An enum TaggedAdminApiResponse that contains the same variants, but +// serializes as a tagged enum. This allows it to be transmitted through +// Garage RPC and deserialized correctly upon receival. +// Conversion from untagged to tagged can be done using the `.tagged()` method. +// // - 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![ @@ -60,6 +72,9 @@ admin_endpoints![ // ********************************************** // Special endpoints +// +// These endpoints don't have associated *Response structs +// because they directly produce an http::Response // ********************************************** #[derive(Serialize, Deserialize)] @@ -153,11 +168,11 @@ pub struct GetClusterHealthResponse { pub struct ConnectClusterNodesRequest(pub Vec); #[derive(Serialize, Deserialize)] -pub struct ConnectClusterNodesResponse(pub Vec); +pub struct ConnectClusterNodesResponse(pub Vec); #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct ConnectClusterNodeResponse { +pub struct ConnectNodeResponse { pub success: bool, pub error: Option, } @@ -331,7 +346,6 @@ pub struct UpdateKeyResponse(pub GetKeyInfoResponse); #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UpdateKeyRequestBody { - // TODO: id (get parameter) goes here pub name: Option, pub allow: Option, pub deny: Option, diff --git a/src/api/admin/api_server.rs b/src/api/admin/api_server.rs index 92da3245..b835322d 100644 --- a/src/api/admin/api_server.rs +++ b/src/api/admin/api_server.rs @@ -176,7 +176,7 @@ impl ApiHandler for AdminApiServer { impl ApiEndpoint for Endpoint { fn name(&self) -> Cow<'static, str> { match self { - Self::Old(endpoint_v1) => Cow::Owned(format!("v1:{}", endpoint_v1.name())), + Self::Old(endpoint_v1) => Cow::Borrowed(endpoint_v1.name()), Self::New(path) => Cow::Owned(path.clone()), } } diff --git a/src/api/admin/cluster.rs b/src/api/admin/cluster.rs index 112cb542..0cfd744a 100644 --- a/src/api/admin/cluster.rs +++ b/src/api/admin/cluster.rs @@ -151,11 +151,11 @@ impl EndpointHandler for ConnectClusterNodesRequest { .await .into_iter() .map(|r| match r { - Ok(()) => ConnectClusterNodeResponse { + Ok(()) => ConnectNodeResponse { success: true, error: None, }, - Err(e) => ConnectClusterNodeResponse { + Err(e) => ConnectNodeResponse { success: false, error: Some(format!("{}", e)), }, diff --git a/src/api/admin/macros.rs b/src/api/admin/macros.rs index d68ba37f..7082577f 100644 --- a/src/api/admin/macros.rs +++ b/src/api/admin/macros.rs @@ -14,7 +14,7 @@ macro_rules! admin_endpoints { )* } - #[derive(Serialize, Deserialize)] + #[derive(Serialize)] #[serde(untagged)] pub enum AdminApiResponse { $( @@ -22,6 +22,13 @@ macro_rules! admin_endpoints { )* } + #[derive(Serialize, Deserialize)] + pub enum TaggedAdminApiResponse { + $( + $endpoint( [<$endpoint Response>] ), + )* + } + impl AdminApiRequest { pub fn name(&self) -> &'static str { match self { @@ -35,6 +42,16 @@ macro_rules! admin_endpoints { } } + impl AdminApiResponse { + fn tagged(self) -> TaggedAdminApiResponse { + match self { + $( + Self::$endpoint(res) => TaggedAdminApiResponse::$endpoint(res), + )* + } + } + } + #[async_trait] impl EndpointHandler for AdminApiRequest { type Response = AdminApiResponse; -- cgit v1.2.3 From 1c03941b192dc1c8418618166293c3fb5b9732a9 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 29 Jan 2025 12:46:20 +0100 Subject: admin api: fix panic on GetKeyInfo with no args --- src/api/admin/key.rs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) (limited to 'src/api') diff --git a/src/api/admin/key.rs b/src/api/admin/key.rs index 3e4201d9..d2f449ed 100644 --- a/src/api/admin/key.rs +++ b/src/api/admin/key.rs @@ -43,15 +43,19 @@ impl EndpointHandler for GetKeyInfoRequest { type Response = GetKeyInfoResponse; async fn handle(self, garage: &Arc) -> Result { - let key = if let Some(id) = self.id { - garage.key_helper().get_existing_key(&id).await? - } else if let Some(search) = self.search { - garage - .key_helper() - .get_existing_matching_key(&search) - .await? - } else { - unreachable!(); + let key = match (self.id, self.search) { + (Some(id), None) => garage.key_helper().get_existing_key(&id).await?, + (None, Some(search)) => { + garage + .key_helper() + .get_existing_matching_key(&search) + .await? + } + _ => { + return Err(Error::bad_request( + "Either id or search must be provided (but not both)", + )); + } }; Ok(key_info_results(garage, key, self.show_secret_key).await?) -- cgit v1.2.3 From 19454c1679352012f1953949d02880e34820039f Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 29 Jan 2025 19:47:37 +0100 Subject: admin api: remove log message --- src/api/admin/api_server.rs | 2 -- 1 file changed, 2 deletions(-) (limited to 'src/api') diff --git a/src/api/admin/api_server.rs b/src/api/admin/api_server.rs index b835322d..d66714db 100644 --- a/src/api/admin/api_server.rs +++ b/src/api/admin/api_server.rs @@ -134,8 +134,6 @@ impl ApiHandler for AdminApiServer { Endpoint::New(_) => AdminApiRequest::from_request(req).await?, }; - info!("Admin request: {}", request.name()); - let required_auth_hash = match request.authorization_type() { Authorization::None => None, -- cgit v1.2.3