aboutsummaryrefslogtreecommitdiff
path: root/src/api/admin
diff options
context:
space:
mode:
Diffstat (limited to 'src/api/admin')
-rw-r--r--src/api/admin/api_server.rs9
-rw-r--r--src/api/admin/bucket.rs24
-rw-r--r--src/api/admin/cluster.rs207
-rw-r--r--src/api/admin/key.rs43
-rw-r--r--src/api/admin/router.rs56
5 files changed, 242 insertions, 97 deletions
diff --git a/src/api/admin/api_server.rs b/src/api/admin/api_server.rs
index 50c79120..cc04d81f 100644
--- a/src/api/admin/api_server.rs
+++ b/src/api/admin/api_server.rs
@@ -276,8 +276,13 @@ impl ApiHandler for AdminApiServer {
Endpoint::RevertClusterLayout => handle_revert_cluster_layout(&self.garage, req).await,
// Keys
Endpoint::ListKeys => handle_list_keys(&self.garage).await,
- Endpoint::GetKeyInfo { id, search } => {
- handle_get_key_info(&self.garage, id, search).await
+ Endpoint::GetKeyInfo {
+ id,
+ search,
+ show_secret_key,
+ } => {
+ let show_secret_key = show_secret_key.map(|x| x == "true").unwrap_or(false);
+ handle_get_key_info(&self.garage, id, search, show_secret_key).await
}
Endpoint::CreateKey => handle_create_key(&self.garage, req).await,
Endpoint::ImportKey => handle_import_key(&self.garage, req).await,
diff --git a/src/api/admin/bucket.rs b/src/api/admin/bucket.rs
index f0a4a9e7..17f46c30 100644
--- a/src/api/admin/bucket.rs
+++ b/src/api/admin/bucket.rs
@@ -14,6 +14,7 @@ use garage_model::bucket_alias_table::*;
use garage_model::bucket_table::*;
use garage_model::garage::Garage;
use garage_model::permission::*;
+use garage_model::s3::mpu_table;
use garage_model::s3::object_table::*;
use crate::admin::error::*;
@@ -124,6 +125,14 @@ async fn bucket_info_results(
.map(|x| x.filtered_values(&garage.system.ring.borrow()))
.unwrap_or_default();
+ let mpu_counters = garage
+ .mpu_counter_table
+ .table
+ .get(&bucket_id, &EmptyKey)
+ .await?
+ .map(|x| x.filtered_values(&garage.system.ring.borrow()))
+ .unwrap_or_default();
+
let mut relevant_keys = HashMap::new();
for (k, _) in bucket
.state
@@ -208,12 +217,12 @@ async fn bucket_info_results(
}
})
.collect::<Vec<_>>(),
- objects: counters.get(OBJECTS).cloned().unwrap_or_default(),
- bytes: counters.get(BYTES).cloned().unwrap_or_default(),
- unfinished_uploads: counters
- .get(UNFINISHED_UPLOADS)
- .cloned()
- .unwrap_or_default(),
+ 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,
@@ -235,6 +244,9 @@ struct GetBucketInfoResult {
objects: i64,
bytes: i64,
unfinished_uploads: i64,
+ unfinished_multipart_uploads: i64,
+ unfinished_multipart_upload_parts: i64,
+ unfinished_multipart_upload_bytes: i64,
quotas: ApiBucketQuotas,
}
diff --git a/src/api/admin/cluster.rs b/src/api/admin/cluster.rs
index 98bf2b5a..c8107b82 100644
--- a/src/api/admin/cluster.rs
+++ b/src/api/admin/cluster.rs
@@ -1,14 +1,13 @@
-use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
-use hyper::{Body, Request, Response, StatusCode};
+use hyper::{Body, Request, Response};
use serde::{Deserialize, Serialize};
use garage_util::crdt::*;
use garage_util::data::*;
-use garage_rpc::layout::*;
+use garage_rpc::layout;
use garage_model::garage::Garage;
@@ -26,26 +25,37 @@ pub async fn handle_get_cluster_status(garage: &Arc<Garage>) -> Result<Response<
.system
.get_known_nodes()
.into_iter()
- .map(|i| {
- (
- hex::encode(i.id),
- KnownNodeResp {
- addr: i.addr,
- is_up: i.is_up,
- last_seen_secs_ago: i.last_seen_secs_ago,
- hostname: i.status.hostname,
- },
- )
+ .map(|i| KnownNodeResp {
+ id: hex::encode(i.id),
+ addr: i.addr,
+ is_up: i.is_up,
+ last_seen_secs_ago: i.last_seen_secs_ago,
+ hostname: i.status.hostname,
})
.collect(),
- layout: get_cluster_layout(garage),
+ layout: format_cluster_layout(&garage.system.get_cluster_layout()),
};
Ok(json_ok_response(&res)?)
}
pub async fn handle_get_cluster_health(garage: &Arc<Garage>) -> Result<Response<Body>, Error> {
+ use garage_rpc::system::ClusterHealthStatus;
let health = garage.system.health();
+ let health = ClusterHealth {
+ status: match health.status {
+ ClusterHealthStatus::Healthy => "healthy",
+ ClusterHealthStatus::Degraded => "degraded",
+ ClusterHealthStatus::Unavailable => "unavailable",
+ },
+ known_nodes: health.known_nodes,
+ connected_nodes: health.connected_nodes,
+ storage_nodes: health.storage_nodes,
+ storage_nodes_ok: health.storage_nodes_ok,
+ partitions: health.partitions,
+ partitions_quorum: health.partitions_quorum,
+ partitions_all_ok: health.partitions_all_ok,
+ };
Ok(json_ok_response(&health)?)
}
@@ -74,33 +84,68 @@ pub async fn handle_connect_cluster_nodes(
}
pub async fn handle_get_cluster_layout(garage: &Arc<Garage>) -> Result<Response<Body>, Error> {
- let res = get_cluster_layout(garage);
+ let res = format_cluster_layout(&garage.system.get_cluster_layout());
Ok(json_ok_response(&res)?)
}
-fn get_cluster_layout(garage: &Arc<Garage>) -> GetClusterLayoutResponse {
- let layout = garage.system.get_cluster_layout();
+fn format_cluster_layout(layout: &layout::ClusterLayout) -> GetClusterLayoutResponse {
+ let roles = layout
+ .roles
+ .items()
+ .iter()
+ .filter_map(|(k, _, v)| v.0.clone().map(|x| (k, x)))
+ .map(|(k, v)| NodeRoleResp {
+ id: hex::encode(k),
+ zone: v.zone.clone(),
+ capacity: v.capacity,
+ tags: v.tags.clone(),
+ })
+ .collect::<Vec<_>>();
+
+ let staged_role_changes = layout
+ .staging_roles
+ .items()
+ .iter()
+ .filter(|(k, _, v)| layout.roles.get(k) != Some(v))
+ .map(|(k, _, v)| match &v.0 {
+ None => NodeRoleChange {
+ id: hex::encode(k),
+ action: NodeRoleChangeEnum::Remove { remove: true },
+ },
+ Some(r) => NodeRoleChange {
+ id: hex::encode(k),
+ action: NodeRoleChangeEnum::Update {
+ zone: r.zone.clone(),
+ capacity: r.capacity,
+ tags: r.tags.clone(),
+ },
+ },
+ })
+ .collect::<Vec<_>>();
GetClusterLayoutResponse {
version: layout.version,
- roles: layout
- .roles
- .items()
- .iter()
- .filter(|(_, _, v)| v.0.is_some())
- .map(|(k, _, v)| (hex::encode(k), v.0.clone()))
- .collect(),
- staged_role_changes: layout
- .staging
- .items()
- .iter()
- .filter(|(k, _, v)| layout.roles.get(k) != Some(v))
- .map(|(k, _, v)| (hex::encode(k), v.0.clone()))
- .collect(),
+ roles,
+ staged_role_changes,
}
}
+// ----
+
+#[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 {
@@ -109,11 +154,19 @@ struct GetClusterStatusResponse {
garage_features: Option<&'static [&'static str]>,
rust_version: &'static str,
db_engine: String,
- known_nodes: HashMap<String, KnownNodeResp>,
+ known_nodes: Vec<KnownNodeResp>,
+ layout: GetClusterLayoutResponse,
+}
+
+#[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>,
@@ -123,18 +176,31 @@ struct ConnectClusterNodesResponse {
#[serde(rename_all = "camelCase")]
struct GetClusterLayoutResponse {
version: u64,
- roles: HashMap<String, Option<NodeRole>>,
- staged_role_changes: HashMap<String, Option<NodeRole>>,
+ 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)]
+#[serde(rename_all = "camelCase")]
struct KnownNodeResp {
+ id: String,
addr: SocketAddr,
is_up: bool,
last_seen_secs_ago: Option<u64>,
hostname: String,
}
+// ---- update functions ----
+
pub async fn handle_update_cluster_layout(
garage: &Arc<Garage>,
req: Request<Body>,
@@ -144,22 +210,35 @@ pub async fn handle_update_cluster_layout(
let mut layout = garage.system.get_cluster_layout();
let mut roles = layout.roles.clone();
- roles.merge(&layout.staging);
+ roles.merge(&layout.staging_roles);
- for (node, role) in updates {
- let node = hex::decode(node).ok_or_bad_request("Invalid node identifier")?;
+ for change in updates {
+ let node = hex::decode(&change.id).ok_or_bad_request("Invalid node identifier")?;
let node = Uuid::try_from(&node).ok_or_bad_request("Invalid node identifier")?;
+ let new_role = match change.action {
+ NodeRoleChangeEnum::Remove { remove: true } => None,
+ NodeRoleChangeEnum::Update {
+ zone,
+ capacity,
+ tags,
+ } => Some(layout::NodeRole {
+ zone,
+ capacity,
+ tags,
+ }),
+ _ => return Err(Error::bad_request("Invalid layout change")),
+ };
+
layout
- .staging
- .merge(&roles.update_mutator(node, NodeRoleV(role)));
+ .staging_roles
+ .merge(&roles.update_mutator(node, layout::NodeRoleV(new_role)));
}
garage.system.update_cluster_layout(&layout).await?;
- Ok(Response::builder()
- .status(StatusCode::NO_CONTENT)
- .body(Body::empty())?)
+ let res = format_cluster_layout(&layout);
+ Ok(json_ok_response(&res)?)
}
pub async fn handle_apply_cluster_layout(
@@ -169,12 +248,15 @@ pub async fn handle_apply_cluster_layout(
let param = parse_json_body::<ApplyRevertLayoutRequest>(req).await?;
let layout = garage.system.get_cluster_layout();
- let layout = layout.apply_staged_changes(Some(param.version))?;
+ let (layout, msg) = layout.apply_staged_changes(Some(param.version))?;
+
garage.system.update_cluster_layout(&layout).await?;
- Ok(Response::builder()
- .status(StatusCode::NO_CONTENT)
- .body(Body::empty())?)
+ let res = ApplyClusterLayoutResponse {
+ message: msg,
+ layout: format_cluster_layout(&layout),
+ };
+ Ok(json_ok_response(&res)?)
}
pub async fn handle_revert_cluster_layout(
@@ -187,14 +269,39 @@ pub async fn handle_revert_cluster_layout(
let layout = layout.revert_staged_changes(Some(param.version))?;
garage.system.update_cluster_layout(&layout).await?;
- Ok(Response::builder()
- .status(StatusCode::NO_CONTENT)
- .body(Body::empty())?)
+ let res = format_cluster_layout(&layout);
+ Ok(json_ok_response(&res)?)
}
-type UpdateClusterLayoutRequest = HashMap<String, Option<NodeRole>>;
+// ----
+
+type UpdateClusterLayoutRequest = Vec<NodeRoleChange>;
#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
struct ApplyRevertLayoutRequest {
version: u64,
}
+
+// ----
+
+#[derive(Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct NodeRoleChange {
+ id: String,
+ #[serde(flatten)]
+ action: NodeRoleChangeEnum,
+}
+
+#[derive(Serialize, Deserialize)]
+#[serde(untagged)]
+enum NodeRoleChangeEnum {
+ #[serde(rename_all = "camelCase")]
+ Remove { remove: bool },
+ #[serde(rename_all = "camelCase")]
+ Update {
+ zone: String,
+ capacity: Option<u64>,
+ tags: Vec<String>,
+ },
+}
diff --git a/src/api/admin/key.rs b/src/api/admin/key.rs
index d74ca361..8d1c6890 100644
--- a/src/api/admin/key.rs
+++ b/src/api/admin/key.rs
@@ -10,7 +10,7 @@ use garage_model::garage::Garage;
use garage_model::key_table::*;
use crate::admin::error::*;
-use crate::helpers::{json_ok_response, parse_json_body};
+use crate::helpers::{is_default, json_ok_response, parse_json_body};
pub async fn handle_list_keys(garage: &Arc<Garage>) -> Result<Response<Body>, Error> {
let res = garage
@@ -34,6 +34,7 @@ pub async fn handle_list_keys(garage: &Arc<Garage>) -> Result<Response<Body>, Er
}
#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
struct ListKeyResultItem {
id: String,
name: String,
@@ -43,6 +44,7 @@ pub async fn handle_get_key_info(
garage: &Arc<Garage>,
id: Option<String>,
search: Option<String>,
+ show_secret_key: bool,
) -> Result<Response<Body>, Error> {
let key = if let Some(id) = id {
garage.key_helper().get_existing_key(&id).await?
@@ -55,7 +57,7 @@ pub async fn handle_get_key_info(
unreachable!();
};
- key_info_results(garage, key).await
+ key_info_results(garage, key, show_secret_key).await
}
pub async fn handle_create_key(
@@ -64,15 +66,16 @@ pub async fn handle_create_key(
) -> Result<Response<Body>, Error> {
let req = parse_json_body::<CreateKeyRequest>(req).await?;
- let key = Key::new(&req.name);
+ let key = Key::new(req.name.as_deref().unwrap_or("Unnamed key"));
garage.key_table.insert(&key).await?;
- key_info_results(garage, key).await
+ key_info_results(garage, key, true).await
}
#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
struct CreateKeyRequest {
- name: String,
+ name: Option<String>,
}
pub async fn handle_import_key(
@@ -86,10 +89,15 @@ pub async fn handle_import_key(
return Err(Error::KeyAlreadyExists(req.access_key_id.to_string()));
}
- let imported_key = Key::import(&req.access_key_id, &req.secret_access_key, &req.name);
+ let imported_key = Key::import(
+ &req.access_key_id,
+ &req.secret_access_key,
+ req.name.as_deref().unwrap_or("Imported key"),
+ )
+ .ok_or_bad_request("Invalid key format")?;
garage.key_table.insert(&imported_key).await?;
- key_info_results(garage, imported_key).await
+ key_info_results(garage, imported_key, false).await
}
#[derive(Deserialize)]
@@ -97,7 +105,7 @@ pub async fn handle_import_key(
struct ImportKeyRequest {
access_key_id: String,
secret_access_key: String,
- name: String,
+ name: Option<String>,
}
pub async fn handle_update_key(
@@ -127,10 +135,11 @@ pub async fn handle_update_key(
garage.key_table.insert(&key).await?;
- key_info_results(garage, key).await
+ key_info_results(garage, key, false).await
}
#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
struct UpdateKeyRequest {
name: Option<String>,
allow: Option<KeyPerm>,
@@ -149,7 +158,11 @@ pub async fn handle_delete_key(garage: &Arc<Garage>, id: String) -> Result<Respo
.body(Body::empty())?)
}
-async fn key_info_results(garage: &Arc<Garage>, key: Key) -> Result<Response<Body>, Error> {
+async fn key_info_results(
+ garage: &Arc<Garage>,
+ key: Key,
+ show_secret: bool,
+) -> Result<Response<Body>, Error> {
let mut relevant_buckets = HashMap::new();
let key_state = key.state.as_option().unwrap();
@@ -178,7 +191,11 @@ async fn key_info_results(garage: &Arc<Garage>, key: Key) -> Result<Response<Bod
let res = GetKeyInfoResult {
name: key_state.name.get().clone(),
access_key_id: key.key_id.clone(),
- secret_access_key: key_state.secret_key.clone(),
+ secret_access_key: if show_secret {
+ Some(key_state.secret_key.clone())
+ } else {
+ None
+ },
permissions: KeyPerm {
create_bucket: *key_state.allow_create_bucket.get(),
},
@@ -224,7 +241,8 @@ async fn key_info_results(garage: &Arc<Garage>, key: Key) -> Result<Response<Bod
struct GetKeyInfoResult {
name: String,
access_key_id: String,
- secret_access_key: String,
+ #[serde(skip_serializing_if = "is_default")]
+ secret_access_key: Option<String>,
permissions: KeyPerm,
buckets: Vec<KeyInfoBucketResult>,
}
@@ -246,6 +264,7 @@ struct KeyInfoBucketResult {
}
#[derive(Serialize, Deserialize, Default)]
+#[serde(rename_all = "camelCase")]
pub(crate) struct ApiBucketKeyPerm {
#[serde(default)]
pub(crate) read: bool,
diff --git a/src/api/admin/router.rs b/src/api/admin/router.rs
index 0225f18b..254aff12 100644
--- a/src/api/admin/router.rs
+++ b/src/api/admin/router.rs
@@ -35,6 +35,7 @@ pub enum Endpoint {
GetKeyInfo {
id: Option<String>,
search: Option<String>,
+ show_secret_key: Option<String>,
},
DeleteKey {
id: String,
@@ -95,37 +96,37 @@ impl Endpoint {
GET "/check" => CheckDomain,
GET "/health" => Health,
GET "/metrics" => Metrics,
- GET "/v0/status" => GetClusterStatus,
- GET "/v0/health" => GetClusterHealth,
- POST "/v0/connect" => ConnectClusterNodes,
+ GET "/v1/status" => GetClusterStatus,
+ GET "/v1/health" => GetClusterHealth,
+ POST ("/v0/connect" | "/v1/connect") => ConnectClusterNodes,
// Layout endpoints
- GET "/v0/layout" => GetClusterLayout,
- POST "/v0/layout" => UpdateClusterLayout,
- POST "/v0/layout/apply" => ApplyClusterLayout,
- POST "/v0/layout/revert" => RevertClusterLayout,
+ GET "/v1/layout" => GetClusterLayout,
+ POST "/v1/layout" => UpdateClusterLayout,
+ POST "/v1/layout/apply" => ApplyClusterLayout,
+ POST ("/v0/layout/revert" | "/v1/layout/revert") => RevertClusterLayout,
// API key endpoints
- GET "/v0/key" if id => GetKeyInfo (query_opt::id, query_opt::search),
- GET "/v0/key" if search => GetKeyInfo (query_opt::id, query_opt::search),
- POST "/v0/key" if id => UpdateKey (query::id),
- POST "/v0/key" => CreateKey,
- POST "/v0/key/import" => ImportKey,
- DELETE "/v0/key" if id => DeleteKey (query::id),
- GET "/v0/key" => ListKeys,
+ GET "/v1/key" if id => GetKeyInfo (query_opt::id, query_opt::search, query_opt::show_secret_key),
+ GET "/v1/key" if search => GetKeyInfo (query_opt::id, query_opt::search, query_opt::show_secret_key),
+ POST "/v1/key" if id => UpdateKey (query::id),
+ POST "/v1/key" => CreateKey,
+ POST "/v1/key/import" => ImportKey,
+ DELETE ("/v0/key" | "/v1/key") if id => DeleteKey (query::id),
+ GET ("/v0/key" | "/v1/key") => ListKeys,
// Bucket endpoints
- GET "/v0/bucket" if id => GetBucketInfo (query_opt::id, query_opt::global_alias),
- GET "/v0/bucket" if global_alias => GetBucketInfo (query_opt::id, query_opt::global_alias),
- GET "/v0/bucket" => ListBuckets,
- POST "/v0/bucket" => CreateBucket,
- DELETE "/v0/bucket" if id => DeleteBucket (query::id),
- PUT "/v0/bucket" if id => UpdateBucket (query::id),
+ GET ("/v0/bucket" | "/v1/bucket") if id => GetBucketInfo (query_opt::id, query_opt::global_alias),
+ GET ("/v0/bucket" | "/v1/bucket") if global_alias => GetBucketInfo (query_opt::id, query_opt::global_alias),
+ GET ("/v0/bucket" | "/v1/bucket") => ListBuckets,
+ POST ("/v0/bucket" | "/v1/bucket") => CreateBucket,
+ DELETE ("/v0/bucket" | "/v1/bucket") if id => DeleteBucket (query::id),
+ PUT ("/v0/bucket" | "/v1/bucket") if id => UpdateBucket (query::id),
// Bucket-key permissions
- POST "/v0/bucket/allow" => BucketAllowKey,
- POST "/v0/bucket/deny" => BucketDenyKey,
+ POST ("/v0/bucket/allow" | "/v1/bucket/allow") => BucketAllowKey,
+ POST ("/v0/bucket/deny" | "/v1/bucket/deny") => BucketDenyKey,
// Bucket aliases
- PUT "/v0/bucket/alias/global" => GlobalAliasBucket (query::id, query::alias),
- DELETE "/v0/bucket/alias/global" => GlobalUnaliasBucket (query::id, query::alias),
- PUT "/v0/bucket/alias/local" => LocalAliasBucket (query::id, query::access_key_id, query::alias),
- DELETE "/v0/bucket/alias/local" => LocalUnaliasBucket (query::id, query::access_key_id, query::alias),
+ PUT ("/v0/bucket/alias/global" | "/v1/bucket/alias/global") => GlobalAliasBucket (query::id, query::alias),
+ DELETE ("/v0/bucket/alias/global" | "/v1/bucket/alias/global") => GlobalUnaliasBucket (query::id, query::alias),
+ PUT ("/v0/bucket/alias/local" | "/v1/bucket/alias/local") => LocalAliasBucket (query::id, query::access_key_id, query::alias),
+ DELETE ("/v0/bucket/alias/local" | "/v1/bucket/alias/local") => LocalUnaliasBucket (query::id, query::access_key_id, query::alias),
]);
if let Some(message) = query.nonempty_message() {
@@ -153,6 +154,7 @@ generateQueryParameters! {
"search" => search,
"globalAlias" => global_alias,
"alias" => alias,
- "accessKeyId" => access_key_id
+ "accessKeyId" => access_key_id,
+ "showSecretKey" => show_secret_key
]
}