admin api: create structs for all requests/responess in src/api/admin/api.rs
This commit is contained in:
parent
27ac0b272c
commit
712ab0c768
6 changed files with 721 additions and 455 deletions
486
src/api/admin/api.rs
Normal file
486
src/api/admin/api.rs
Normal 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;
|
|
@ -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,
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
|
|
|
@ -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>;
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue