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,8 +157,7 @@ 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
@ -187,7 +168,7 @@ async fn bucket_info_results(
.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| {
GetBucketInfoWebsiteResult { GetBucketInfoWebsiteResponse {
index_document: wsc.index_document, index_document: wsc.index_document,
error_document: wsc.error_document, error_document: wsc.error_document,
} }
@ -233,40 +214,6 @@ async fn bucket_info_results(
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,11 +11,23 @@ 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]
impl EndpointHandler for GetClusterStatusRequest {
type Response = GetClusterStatusResponse;
async fn handle(self, garage: &Arc<Garage>) -> Result<GetClusterStatusResponse, Error> {
let layout = garage.system.cluster_layout(); let layout = garage.system.cluster_layout();
let mut nodes = garage let mut nodes = garage
.system .system
@ -31,12 +42,11 @@ pub async fn handle_get_cluster_status(garage: &Arc<Garage>) -> Result<Response<
hostname: i.status.hostname, hostname: i.status.hostname,
is_up: i.is_up, is_up: i.is_up,
last_seen_secs_ago: i.last_seen_secs_ago, last_seen_secs_ago: i.last_seen_secs_ago,
data_partition: i data_partition: i.status.data_disk_avail.map(|(avail, total)| {
.status FreeSpaceResp {
.data_disk_avail
.map(|(avail, total)| FreeSpaceResp {
available: avail, available: avail,
total, total,
}
}), }),
metadata_partition: i.status.meta_disk_avail.map(|(avail, total)| { metadata_partition: i.status.meta_disk_avail.map(|(avail, total)| {
FreeSpaceResp { FreeSpaceResp {
@ -102,7 +112,7 @@ pub async fn handle_get_cluster_status(garage: &Arc<Garage>) -> Result<Response<
let mut nodes = nodes.into_values().collect::<Vec<_>>(); let mut nodes = nodes.into_values().collect::<Vec<_>>();
nodes.sort_by(|x, y| x.id.cmp(&y.id)); nodes.sort_by(|x, y| x.id.cmp(&y.id));
let res = GetClusterStatusResponse { Ok(GetClusterStatusResponse {
node: hex::encode(garage.system.id), node: hex::encode(garage.system.id),
garage_version: garage_util::version::garage_version(), garage_version: garage_util::version::garage_version(),
garage_features: garage_util::version::garage_features(), garage_features: garage_util::version::garage_features(),
@ -110,15 +120,18 @@ pub async fn handle_get_cluster_status(garage: &Arc<Garage>) -> Result<Response<
db_engine: garage.db.engine(), db_engine: garage.db.engine(),
layout_version: layout.current().version, layout_version: layout.current().version,
nodes, nodes,
}; })
}
Ok(json_ok_response(&res)?)
} }
pub async fn handle_get_cluster_health(garage: &Arc<Garage>) -> Result<Response<ResBody>, Error> { #[async_trait]
impl EndpointHandler for GetClusterHealthRequest {
type Response = GetClusterHealthResponse;
async fn handle(self, garage: &Arc<Garage>) -> Result<GetClusterHealthResponse, Error> {
use garage_rpc::system::ClusterHealthStatus; use garage_rpc::system::ClusterHealthStatus;
let health = garage.system.health(); let health = garage.system.health();
let health = ClusterHealth { let health = GetClusterHealthResponse {
status: match health.status { status: match health.status {
ClusterHealthStatus::Healthy => "healthy", ClusterHealthStatus::Healthy => "healthy",
ClusterHealthStatus::Degraded => "degraded", ClusterHealthStatus::Degraded => "degraded",
@ -132,31 +145,42 @@ pub async fn handle_get_cluster_health(garage: &Arc<Garage>) -> Result<Response<
partitions_quorum: health.partitions_quorum, partitions_quorum: health.partitions_quorum,
partitions_all_ok: health.partitions_all_ok, partitions_all_ok: health.partitions_all_ok,
}; };
Ok(json_ok_response(&health)?) 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?;
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 .await
.into_iter() .into_iter()
.map(|r| match r { .map(|r| match r {
Ok(()) => ConnectClusterNodesResponse { Ok(()) => ConnectClusterNodeResponse {
success: true, success: true,
error: None, error: None,
}, },
Err(e) => ConnectClusterNodesResponse { Err(e) => ConnectClusterNodeResponse {
success: false, success: false,
error: Some(format!("{}", e)), error: Some(format!("{}", e)),
}, },
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
Ok(ConnectClusterNodesResponse(res))
Ok(json_ok_response(&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> {
@ -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>;
}