aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAlex Auvolat <lx@deuxfleurs.fr>2025-01-27 23:13:01 +0100
committerAlex Auvolat <lx@deuxfleurs.fr>2025-01-29 19:26:16 +0100
commitc1eb1610bab4d0d689dae9389f3fc10c0ab0efdc (patch)
tree6dbcdbe1cad3426e2e4dc2db19cb68c98bb8dcb3 /src
parent5560a963e048f6bb000fc37b7e7ad73dbe96f3ab (diff)
downloadgarage-c1eb1610bab4d0d689dae9389f3fc10c0ab0efdc.tar.gz
garage-c1eb1610bab4d0d689dae9389f3fc10c0ab0efdc.zip
admin api: create structs for all requests/responess in src/api/admin/api.rs
Diffstat (limited to 'src')
-rw-r--r--src/api/admin/api.rs486
-rw-r--r--src/api/admin/api_server.rs12
-rw-r--r--src/api/admin/bucket.rs210
-rw-r--r--src/api/admin/cluster.rs374
-rw-r--r--src/api/admin/key.rs78
-rw-r--r--src/api/admin/mod.rs16
6 files changed, 721 insertions, 455 deletions
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<NodeResp>,
+}
+
+#[derive(Serialize, Default)]
+#[serde(rename_all = "camelCase")]
+pub struct NodeResp {
+ pub id: String,
+ pub role: Option<NodeRoleResp>,
+ pub addr: Option<SocketAddr>,
+ pub hostname: Option<String>,
+ pub is_up: bool,
+ pub last_seen_secs_ago: Option<u64>,
+ pub draining: bool,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub data_partition: Option<FreeSpaceResp>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub metadata_partition: Option<FreeSpaceResp>,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct NodeRoleResp {
+ pub id: String,
+ pub zone: String,
+ pub capacity: Option<u64>,
+ pub tags: Vec<String>,
+}
+
+#[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<String>);
+
+#[derive(Serialize)]
+pub struct ConnectClusterNodesResponse(pub Vec<ConnectClusterNodeResponse>);
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ConnectClusterNodeResponse {
+ pub success: bool,
+ pub error: Option<String>,
+}
+
+// ---- GetClusterLayout ----
+
+pub struct GetClusterLayoutRequest;
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct GetClusterLayoutResponse {
+ pub version: u64,
+ pub roles: Vec<NodeRoleResp>,
+ pub staged_role_changes: Vec<NodeRoleChange>,
+}
+
+#[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<u64>,
+ tags: Vec<String>,
+ },
+}
+
+// ---- UpdateClusterLayout ----
+
+#[derive(Deserialize)]
+pub struct UpdateClusterLayoutRequest(pub Vec<NodeRoleChange>);
+
+#[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<String>,
+ 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<ListKeysResponseItem>);
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ListKeysResponseItem {
+ pub id: String,
+ pub name: String,
+}
+
+// ---- GetKeyInfo ----
+
+pub struct GetKeyInfoRequest {
+ pub id: Option<String>,
+ pub search: Option<String>,
+ 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<String>,
+ pub permissions: KeyPerm,
+ pub buckets: Vec<KeyInfoBucketResponse>,
+}
+
+#[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<String>,
+ pub local_aliases: Vec<String>,
+ 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<String>,
+}
+
+#[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<String>,
+}
+
+#[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<String>,
+ pub allow: Option<KeyPerm>,
+ pub deny: Option<KeyPerm>,
+}
+
+#[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<ListBucketsResponseItem>);
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ListBucketsResponseItem {
+ pub id: String,
+ pub global_aliases: Vec<String>,
+ pub local_aliases: Vec<BucketLocalAlias>,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct BucketLocalAlias {
+ pub access_key_id: String,
+ pub alias: String,
+}
+
+// ---- GetBucketInfo ----
+
+pub struct GetBucketInfoRequest {
+ pub id: Option<String>,
+ pub global_alias: Option<String>,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct GetBucketInfoResponse {
+ pub id: String,
+ pub global_aliases: Vec<String>,
+ pub website_access: bool,
+ #[serde(default)]
+ pub website_config: Option<GetBucketInfoWebsiteResponse>,
+ pub keys: Vec<GetBucketInfoKey>,
+ 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<String>,
+}
+
+#[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<String>,
+}
+
+#[derive(Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ApiBucketQuotas {
+ pub max_size: Option<u64>,
+ pub max_objects: Option<u64>,
+}
+
+// ---- CreateBucket ----
+
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CreateBucketRequest {
+ pub global_alias: Option<String>,
+ pub local_alias: Option<CreateBucketLocalAlias>,
+}
+
+#[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<UpdateBucketWebsiteAccess>,
+ pub quotas: Option<ApiBucketQuotas>,
+}
+
+#[derive(Serialize)]
+pub struct UpdateBucketResponse(GetBucketInfoResponse);
+
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct UpdateBucketWebsiteAccess {
+ pub enabled: bool,
+ pub index_document: Option<String>,
+ pub error_document: Option<String>,
+}
+
+// ---- 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<Error>;
@@ -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<Garage>) -> Result<Response<ResBod
.into_iter()
.map(|b| {
let state = b.state.as_option().unwrap();
- ListBucketResultItem {
+ ListBucketsResponseItem {
id: hex::encode(b.id),
global_aliases: state
.aliases
@@ -65,28 +69,6 @@ pub async fn handle_list_buckets(garage: &Arc<Garage>) -> Result<Response<ResBod
Ok(json_ok_response(&res)?)
}
-#[derive(Serialize)]
-#[serde(rename_all = "camelCase")]
-struct ListBucketResultItem {
- id: String,
- global_aliases: Vec<String>,
- local_aliases: Vec<BucketLocalAlias>,
-}
-
-#[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<u64>,
- max_objects: Option<u64>,
-}
-
pub async fn handle_get_bucket_info(
garage: &Arc<Garage>,
id: Option<String>,
@@ -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::<Vec<_>>(),
- 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::<Vec<_>>(),
+ 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::<Vec<_>>(),
}
- }),
- 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::<Vec<_>>(),
- }
- })
- .collect::<Vec<_>>(),
- 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::<Vec<_>>(),
+ 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<String>,
- website_access: bool,
- #[serde(default)]
- website_config: Option<GetBucketInfoWebsiteResult>,
- keys: Vec<GetBucketInfoKey>,
- 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<String>,
-}
-
-#[derive(Serialize)]
-#[serde(rename_all = "camelCase")]
-struct GetBucketInfoKey {
- access_key_id: String,
- name: String,
- permissions: ApiBucketKeyPerm,
- bucket_local_aliases: Vec<String>,
-}
-
pub async fn handle_create_bucket(
garage: &Arc<Garage>,
req: Request<IncomingBody>,
@@ -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<String>,
- local_alias: Option<CreateBucketLocalAlias>,
-}
-
-#[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<Garage>,
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<UpdateBucketWebsiteAccess>,
- quotas: Option<ApiBucketQuotas>,
-}
-
-#[derive(Deserialize)]
-#[serde(rename_all = "camelCase")]
-struct UpdateBucketWebsiteAccess {
- enabled: bool,
- index_document: Option<String>,
- error_document: Option<String>,
-}
-
// ---- 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<Garage>) -> Result<Response<ResBody>, 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<Garage>) -> Result<GetClusterStatusResponse, 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,
+ }
}),
- metadata_partition: i.status.meta_disk_avail.map(|(avail, total)| {
- FreeSpaceResp {
- available: avail,
- total,
- }
- }),
- ..Default::default()
- },
- )
- })
- .collect::<HashMap<_, _>>();
-
- 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::<HashMap<_, _>>();
- 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::<Vec<_>>();
- 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::<Vec<_>>();
+ 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<Garage>) -> Result<Response<ResBody>, 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<Garage>) -> Result<GetClusterHealthResponse, Error> {
+ 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<Garage>,
req: Request<IncomingBody>,
) -> Result<Response<ResBody>, Error> {
- let req = parse_json_body::<Vec<String>, _, 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::<Vec<_>>();
+ let req = parse_json_body::<ConnectClusterNodesRequest, _, Error>(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<Garage>) -> Result<ConnectClusterNodesResponse, Error> {
+ 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::<Vec<_>>();
+ Ok(ConnectClusterNodesResponse(res))
+ }
+}
+
pub async fn handle_get_cluster_layout(garage: &Arc<Garage>) -> Result<Response<ResBody>, 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<NodeResp>,
-}
-
-#[derive(Serialize)]
-#[serde(rename_all = "camelCase")]
-struct ApplyClusterLayoutResponse {
- message: Vec<String>,
- layout: GetClusterLayoutResponse,
-}
-
-#[derive(Serialize)]
-#[serde(rename_all = "camelCase")]
-struct ConnectClusterNodesResponse {
- success: bool,
- error: Option<String>,
-}
-
-#[derive(Serialize)]
-#[serde(rename_all = "camelCase")]
-struct GetClusterLayoutResponse {
- version: u64,
- roles: Vec<NodeRoleResp>,
- staged_role_changes: Vec<NodeRoleChange>,
-}
-
-#[derive(Serialize)]
-#[serde(rename_all = "camelCase")]
-struct NodeRoleResp {
- id: String,
- zone: String,
- capacity: Option<u64>,
- tags: Vec<String>,
-}
-
-#[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<NodeRoleResp>,
- addr: Option<SocketAddr>,
- hostname: Option<String>,
- is_up: bool,
- last_seen_secs_ago: Option<u64>,
- draining: bool,
- #[serde(skip_serializing_if = "Option::is_none")]
- data_partition: Option<FreeSpaceResp>,
- #[serde(skip_serializing_if = "Option::is_none")]
- metadata_partition: Option<FreeSpaceResp>,
-}
-
// ---- 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<Garage>,
req: Request<IncomingBody>,
) -> Result<Response<ResBody>, Error> {
- let param = parse_json_body::<ApplyLayoutRequest, _, Error>(req).await?;
+ let param = parse_json_body::<ApplyClusterLayoutRequest, _, Error>(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<NodeRoleChange>;
-
-#[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<u64>,
- tags: Vec<String>,
- },
-}
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<Garage>) -> Result<Response<ResBody>,
)
.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<Garage>) -> Result<Response<ResBody>,
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<Garage>,
id: Option<String>,
@@ -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<String>,
-}
-
pub async fn handle_import_key(
garage: &Arc<Garage>,
req: Request<IncomingBody>,
@@ -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<String>,
-}
-
pub async fn handle_update_key(
garage: &Arc<Garage>,
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<String>,
- allow: Option<KeyPerm>,
- deny: Option<KeyPerm>,
-}
-
pub async fn handle_delete_key(
garage: &Arc<Garage>,
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<String>,
- permissions: KeyPerm,
- buckets: Vec<KeyInfoBucketResult>,
-}
-
-#[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<String>,
- local_aliases: Vec<String>,
- 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<Garage>) -> Result<Self::Response, error::Error>;
+}