admin api: create structs for all requests/responess in src/api/admin/api.rs

This commit is contained in:
Alex 2025-01-27 23:13:01 +01:00
parent 27ac0b272c
commit 712ab0c768
6 changed files with 721 additions and 455 deletions

486
src/api/admin/api.rs Normal file
View file

@ -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;

View file

@ -22,12 +22,14 @@ use garage_util::socket_address::UnixOrTCPSocketAddress;
use crate::generic_server::*; use crate::generic_server::*;
use crate::admin::api::*;
use crate::admin::bucket::*; use crate::admin::bucket::*;
use crate::admin::cluster::*; use crate::admin::cluster::*;
use crate::admin::error::*; use crate::admin::error::*;
use crate::admin::key::*; use crate::admin::key::*;
use crate::admin::router_v0; use crate::admin::router_v0;
use crate::admin::router_v1::{Authorization, Endpoint}; use crate::admin::router_v1::{Authorization, Endpoint};
use crate::admin::EndpointHandler;
use crate::helpers::*; use crate::helpers::*;
pub type ResBody = BoxBody<Error>; pub type ResBody = BoxBody<Error>;
@ -269,8 +271,14 @@ impl ApiHandler for AdminApiServer {
Endpoint::CheckDomain => self.handle_check_domain(req).await, Endpoint::CheckDomain => self.handle_check_domain(req).await,
Endpoint::Health => self.handle_health(), Endpoint::Health => self.handle_health(),
Endpoint::Metrics => self.handle_metrics(), Endpoint::Metrics => self.handle_metrics(),
Endpoint::GetClusterStatus => handle_get_cluster_status(&self.garage).await, Endpoint::GetClusterStatus => GetClusterStatusRequest
Endpoint::GetClusterHealth => handle_get_cluster_health(&self.garage).await, .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, Endpoint::ConnectClusterNodes => handle_connect_cluster_nodes(&self.garage, req).await,
// Layout // Layout
Endpoint::GetClusterLayout => handle_get_cluster_layout(&self.garage).await, Endpoint::GetClusterLayout => handle_get_cluster_layout(&self.garage).await,

View file

@ -2,7 +2,6 @@ use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use hyper::{body::Incoming as IncomingBody, Request, Response, StatusCode}; use hyper::{body::Incoming as IncomingBody, Request, Response, StatusCode};
use serde::{Deserialize, Serialize};
use garage_util::crdt::*; use garage_util::crdt::*;
use garage_util::data::*; use garage_util::data::*;
@ -17,9 +16,14 @@ use garage_model::permission::*;
use garage_model::s3::mpu_table; use garage_model::s3::mpu_table;
use garage_model::s3::object_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::api_server::ResBody;
use crate::admin::error::*; use crate::admin::error::*;
use crate::admin::key::ApiBucketKeyPerm;
use crate::common_error::CommonError; use crate::common_error::CommonError;
use crate::helpers::*; use crate::helpers::*;
@ -39,7 +43,7 @@ pub async fn handle_list_buckets(garage: &Arc<Garage>) -> Result<Response<ResBod
.into_iter() .into_iter()
.map(|b| { .map(|b| {
let state = b.state.as_option().unwrap(); let state = b.state.as_option().unwrap();
ListBucketResultItem { ListBucketsResponseItem {
id: hex::encode(b.id), id: hex::encode(b.id),
global_aliases: state global_aliases: state
.aliases .aliases
@ -65,28 +69,6 @@ pub async fn handle_list_buckets(garage: &Arc<Garage>) -> Result<Response<ResBod
Ok(json_ok_response(&res)?) 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( pub async fn handle_get_bucket_info(
garage: &Arc<Garage>, garage: &Arc<Garage>,
id: Option<String>, id: Option<String>,
@ -175,98 +157,63 @@ async fn bucket_info_results(
let state = bucket.state.as_option().unwrap(); let state = bucket.state.as_option().unwrap();
let quotas = state.quotas.get(); let quotas = state.quotas.get();
let res = let res = GetBucketInfoResponse {
GetBucketInfoResult { id: hex::encode(bucket.id),
id: hex::encode(bucket.id), global_aliases: state
global_aliases: state .aliases
.aliases .items()
.items() .iter()
.iter() .filter(|(_, _, a)| *a)
.filter(|(_, _, a)| *a) .map(|(n, _, _)| n.to_string())
.map(|(n, _, _)| n.to_string()) .collect::<Vec<_>>(),
.collect::<Vec<_>>(), website_access: state.website_config.get().is_some(),
website_access: state.website_config.get().is_some(), website_config: state.website_config.get().clone().map(|wsc| {
website_config: state.website_config.get().clone().map(|wsc| { GetBucketInfoWebsiteResponse {
GetBucketInfoWebsiteResult { index_document: wsc.index_document,
index_document: wsc.index_document, error_document: wsc.error_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 .collect::<Vec<_>>(),
.into_values() objects: *counters.get(OBJECTS).unwrap_or(&0),
.map(|key| { bytes: *counters.get(BYTES).unwrap_or(&0),
let p = key.state.as_option().unwrap(); unfinished_uploads: *counters.get(UNFINISHED_UPLOADS).unwrap_or(&0),
GetBucketInfoKey { unfinished_multipart_uploads: *mpu_counters.get(mpu_table::UPLOADS).unwrap_or(&0),
access_key_id: key.key_id, unfinished_multipart_upload_parts: *mpu_counters.get(mpu_table::PARTS).unwrap_or(&0),
name: p.name.get().to_string(), unfinished_multipart_upload_bytes: *mpu_counters.get(mpu_table::BYTES).unwrap_or(&0),
permissions: p quotas: ApiBucketQuotas {
.authorized_buckets max_size: quotas.max_size,
.get(&bucket.id) max_objects: quotas.max_objects,
.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,
},
};
Ok(json_ok_response(&res)?) 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( pub async fn handle_create_bucket(
garage: &Arc<Garage>, garage: &Arc<Garage>,
req: Request<IncomingBody>, req: Request<IncomingBody>,
@ -336,22 +283,6 @@ pub async fn handle_create_bucket(
bucket_info_results(garage, bucket.id).await 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( pub async fn handle_delete_bucket(
garage: &Arc<Garage>, garage: &Arc<Garage>,
id: String, id: String,
@ -446,21 +377,6 @@ pub async fn handle_update_bucket(
bucket_info_results(garage, bucket_id).await 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 ---- // ---- BUCKET/KEY PERMISSIONS ----
pub async fn handle_bucket_change_key_perm( 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 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 ---- // ---- BUCKET ALIASES ----
pub async fn handle_global_alias_bucket( pub async fn handle_global_alias_bucket(

View file

@ -1,9 +1,8 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait;
use hyper::{body::Incoming as IncomingBody, Request, Response}; use hyper::{body::Incoming as IncomingBody, Request, Response};
use serde::{Deserialize, Serialize};
use garage_util::crdt::*; use garage_util::crdt::*;
use garage_util::data::*; use garage_util::data::*;
@ -12,153 +11,178 @@ use garage_rpc::layout;
use garage_model::garage::Garage; 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::api_server::ResBody;
use crate::admin::error::*; use crate::admin::error::*;
use crate::admin::EndpointHandler;
use crate::helpers::{json_ok_response, parse_json_body}; use crate::helpers::{json_ok_response, parse_json_body};
pub async fn handle_get_cluster_status(garage: &Arc<Garage>) -> Result<Response<ResBody>, Error> { #[async_trait]
let layout = garage.system.cluster_layout(); impl EndpointHandler for GetClusterStatusRequest {
let mut nodes = garage type Response = GetClusterStatusResponse;
.system
.get_known_nodes() async fn handle(self, garage: &Arc<Garage>) -> Result<GetClusterStatusResponse, Error> {
.into_iter() let layout = garage.system.cluster_layout();
.map(|i| { let mut nodes = garage
( .system
i.id, .get_known_nodes()
NodeResp { .into_iter()
id: hex::encode(i.id), .map(|i| {
addr: i.addr, (
hostname: i.status.hostname, i.id,
is_up: i.is_up, NodeResp {
last_seen_secs_ago: i.last_seen_secs_ago, id: hex::encode(i.id),
data_partition: i addr: i.addr,
.status hostname: i.status.hostname,
.data_disk_avail is_up: i.is_up,
.map(|(avail, total)| FreeSpaceResp { last_seen_secs_ago: i.last_seen_secs_ago,
available: avail, data_partition: i.status.data_disk_avail.map(|(avail, total)| {
total, FreeSpaceResp {
available: avail,
total,
}
}), }),
metadata_partition: i.status.meta_disk_avail.map(|(avail, total)| { metadata_partition: i.status.meta_disk_avail.map(|(avail, total)| {
FreeSpaceResp { FreeSpaceResp {
available: avail, available: avail,
total, total,
} }
}), }),
..Default::default() ..Default::default()
}, },
) )
}) })
.collect::<HashMap<_, _>>(); .collect::<HashMap<_, _>>();
for (id, _, role) in layout.current().roles.items().iter() { 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);
}
}
}
}
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 let layout::NodeRoleV(Some(r)) = role {
if r.capacity.is_some() { let role = NodeRoleResp {
if let Some(n) = nodes.get_mut(id) { id: hex::encode(id),
if n.role.is_none() { zone: r.zone.to_string(),
n.draining = true; capacity: r.capacity,
} tags: r.tags.clone(),
} else { };
match nodes.get_mut(id) {
None => {
nodes.insert( nodes.insert(
*id, *id,
NodeResp { NodeResp {
id: hex::encode(id), id: hex::encode(id),
draining: true, role: Some(role),
..Default::default() ..Default::default()
}, },
); );
} }
Some(n) => {
n.role = Some(role);
}
} }
} }
} }
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()
},
);
}
}
}
}
}
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,
})
} }
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,
};
Ok(json_ok_response(&res)?)
} }
pub async fn handle_get_cluster_health(garage: &Arc<Garage>) -> Result<Response<ResBody>, Error> { #[async_trait]
use garage_rpc::system::ClusterHealthStatus; impl EndpointHandler for GetClusterHealthRequest {
let health = garage.system.health(); type Response = GetClusterHealthResponse;
let health = ClusterHealth {
status: match health.status { async fn handle(self, garage: &Arc<Garage>) -> Result<GetClusterHealthResponse, Error> {
ClusterHealthStatus::Healthy => "healthy", use garage_rpc::system::ClusterHealthStatus;
ClusterHealthStatus::Degraded => "degraded", let health = garage.system.health();
ClusterHealthStatus::Unavailable => "unavailable", let health = GetClusterHealthResponse {
}, status: match health.status {
known_nodes: health.known_nodes, ClusterHealthStatus::Healthy => "healthy",
connected_nodes: health.connected_nodes, ClusterHealthStatus::Degraded => "degraded",
storage_nodes: health.storage_nodes, ClusterHealthStatus::Unavailable => "unavailable",
storage_nodes_ok: health.storage_nodes_ok, },
partitions: health.partitions, known_nodes: health.known_nodes,
partitions_quorum: health.partitions_quorum, connected_nodes: health.connected_nodes,
partitions_all_ok: health.partitions_all_ok, storage_nodes: health.storage_nodes,
}; storage_nodes_ok: health.storage_nodes_ok,
Ok(json_ok_response(&health)?) partitions: health.partitions,
partitions_quorum: health.partitions_quorum,
partitions_all_ok: health.partitions_all_ok,
};
Ok(health)
}
} }
pub async fn handle_connect_cluster_nodes( pub async fn handle_connect_cluster_nodes(
garage: &Arc<Garage>, garage: &Arc<Garage>,
req: Request<IncomingBody>, req: Request<IncomingBody>,
) -> Result<Response<ResBody>, Error> { ) -> Result<Response<ResBody>, Error> {
let req = parse_json_body::<Vec<String>, _, Error>(req).await?; let req = parse_json_body::<ConnectClusterNodesRequest, _, Error>(req).await?;
let res = futures::future::join_all(req.iter().map(|node| garage.system.connect(node))) let res = req.handle(garage).await?;
.await
.into_iter()
.map(|r| match r {
Ok(()) => ConnectClusterNodesResponse {
success: true,
error: None,
},
Err(e) => ConnectClusterNodesResponse {
success: false,
error: Some(format!("{}", e)),
},
})
.collect::<Vec<_>>();
Ok(json_ok_response(&res)?) 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> { pub async fn handle_get_cluster_layout(garage: &Arc<Garage>) -> Result<Response<ResBody>, Error> {
let res = format_cluster_layout(garage.system.cluster_layout().inner()); 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 ---- // ---- update functions ----
pub async fn handle_update_cluster_layout( 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(); let mut roles = layout.current().roles.clone();
roles.merge(&layout.staging.get().roles); 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 = 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 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>, garage: &Arc<Garage>,
req: Request<IncomingBody>, req: Request<IncomingBody>,
) -> Result<Response<ResBody>, Error> { ) -> 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 = garage.system.cluster_layout().inner().clone();
let (layout, msg) = layout.apply_staged_changes(Some(param.version))?; 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); let res = format_cluster_layout(&layout);
Ok(json_ok_response(&res)?) 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>,
},
}

View file

@ -2,13 +2,16 @@ use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use hyper::{body::Incoming as IncomingBody, Request, Response, StatusCode}; use hyper::{body::Incoming as IncomingBody, Request, Response, StatusCode};
use serde::{Deserialize, Serialize};
use garage_table::*; use garage_table::*;
use garage_model::garage::Garage; use garage_model::garage::Garage;
use garage_model::key_table::*; 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::api_server::ResBody;
use crate::admin::error::*; use crate::admin::error::*;
use crate::helpers::*; use crate::helpers::*;
@ -25,7 +28,7 @@ pub async fn handle_list_keys(garage: &Arc<Garage>) -> Result<Response<ResBody>,
) )
.await? .await?
.iter() .iter()
.map(|k| ListKeyResultItem { .map(|k| ListKeysResponseItem {
id: k.key_id.to_string(), id: k.key_id.to_string(),
name: k.params().unwrap().name.get().clone(), 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)?) Ok(json_ok_response(&res)?)
} }
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ListKeyResultItem {
id: String,
name: String,
}
pub async fn handle_get_key_info( pub async fn handle_get_key_info(
garage: &Arc<Garage>, garage: &Arc<Garage>,
id: Option<String>, id: Option<String>,
@ -73,12 +69,6 @@ pub async fn handle_create_key(
key_info_results(garage, key, true).await key_info_results(garage, key, true).await
} }
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct CreateKeyRequest {
name: Option<String>,
}
pub async fn handle_import_key( pub async fn handle_import_key(
garage: &Arc<Garage>, garage: &Arc<Garage>,
req: Request<IncomingBody>, req: Request<IncomingBody>,
@ -101,14 +91,6 @@ pub async fn handle_import_key(
key_info_results(garage, imported_key, false).await 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( pub async fn handle_update_key(
garage: &Arc<Garage>, garage: &Arc<Garage>,
id: String, id: String,
@ -139,14 +121,6 @@ pub async fn handle_update_key(
key_info_results(garage, key, false).await 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( pub async fn handle_delete_key(
garage: &Arc<Garage>, garage: &Arc<Garage>,
id: String, id: String,
@ -192,7 +166,7 @@ async fn key_info_results(
} }
} }
let res = GetKeyInfoResult { let res = GetKeyInfoResponse {
name: key_state.name.get().clone(), name: key_state.name.get().clone(),
access_key_id: key.key_id.clone(), access_key_id: key.key_id.clone(),
secret_access_key: if show_secret { secret_access_key: if show_secret {
@ -207,7 +181,7 @@ async fn key_info_results(
.into_values() .into_values()
.map(|bucket| { .map(|bucket| {
let state = bucket.state.as_option().unwrap(); let state = bucket.state.as_option().unwrap();
KeyInfoBucketResult { KeyInfoBucketResponse {
id: hex::encode(bucket.id), id: hex::encode(bucket.id),
global_aliases: state global_aliases: state
.aliases .aliases
@ -239,41 +213,3 @@ async fn key_info_results(
Ok(json_ok_response(&res)?) 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,
}

View file

@ -1,8 +1,24 @@
pub mod api_server; pub mod api_server;
mod error; mod error;
pub mod api;
mod router_v0; mod router_v0;
mod router_v1; mod router_v1;
mod bucket; mod bucket;
mod cluster; mod cluster;
mod key; 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>;
}