aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/drafts/admin-api.md114
-rw-r--r--src/api/admin/api_server.rs8
-rw-r--r--src/api/admin/bucket.rs208
-rw-r--r--src/api/admin/key.rs16
-rw-r--r--src/api/admin/mod.rs1
-rw-r--r--src/api/admin/router.rs21
6 files changed, 355 insertions, 13 deletions
diff --git a/doc/drafts/admin-api.md b/doc/drafts/admin-api.md
index 840dd4f7..edfdeae7 100644
--- a/doc/drafts/admin-api.md
+++ b/doc/drafts/admin-api.md
@@ -339,3 +339,117 @@ All fields (`name`, `allow` and `deny`) are optionnal.
If they are present, the corresponding modifications are applied to the key, otherwise nothing is changed.
The possible flags in `allow` and `deny` are: `createBucket`.
+
+## Bucket operations
+
+### ListBuckets `GET /bucket`
+
+Returns all storage buckets in the cluster.
+
+Example response:
+
+```json
+[
+ {
+ "id": "70dc3bed7fe83a75e46b66e7ddef7d56e65f3c02f9f80b6749fb97eccb5e1033",
+ "globalAliases": [
+ "test2"
+ ],
+ "localAliases": []
+ },
+ {
+ "id": "96470e0df00ec28807138daf01915cfda2bee8eccc91dea9558c0b4855b5bf95",
+ "globalAliases": [
+ "alex"
+ ],
+ "localAliases": []
+ },
+ {
+ "id": "d7452a935e663fc1914f3a5515163a6d3724010ce8dfd9e4743ca8be5974f995",
+ "globalAliases": [
+ "test3"
+ ],
+ "localAliases": []
+ },
+ {
+ "id": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b",
+ "globalAliases": [],
+ "localAliases": [
+ {
+ "accessKeyId": "GK31c2f218a2e44f485b94239e",
+ "alias": "test"
+ }
+ ]
+ }
+]
+```
+
+### GetBucketInfo `GET /bucket?id=<bucket id>`
+### GetBucketInfo `GET /bucket?globalAlias=<alias>`
+
+Returns information about the requested storage bucket.
+
+If `id` is set, the bucket is looked up using its exact identifier.
+If `globalAlias` is set, the bucket is looked up using its global alias.
+(both are fast)
+
+Example response:
+
+```json
+{
+ "id": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b",
+ "globalAliases": [
+ "alex"
+ ],
+ "keys": [
+ {
+ "accessKeyId": "GK31c2f218a2e44f485b94239e",
+ "name": "alex",
+ "permissions": {
+ "read": true,
+ "write": true,
+ "owner": true
+ },
+ "bucketLocalAliases": [
+ "test"
+ ]
+ }
+ ]
+}
+```
+
+### CreateBucket `POST /bucket`
+
+Creates a new storage bucket.
+
+Request body format:
+
+```json
+{
+ "globalAlias": "NameOfMyBucket"
+}
+```
+
+OR
+
+```json
+{
+ "localAlias": {
+ "key": "GK31c2f218a2e44f485b94239e",
+ "alias": "NameOfMyBucket"
+ }
+}
+```
+
+OR
+
+```json
+{}
+```
+
+Creates a new bucket, either with a global alias, a local one,
+or no alias at all.
+
+### DeleteBucket `DELETE /bucket?id=<bucket id>`
+
+Deletes a storage bucket. A bucket cannot be deleted if it is not empty.
diff --git a/src/api/admin/api_server.rs b/src/api/admin/api_server.rs
index 952f6a73..8e5a8c6c 100644
--- a/src/api/admin/api_server.rs
+++ b/src/api/admin/api_server.rs
@@ -18,6 +18,7 @@ use garage_util::error::Error as GarageError;
use crate::error::*;
use crate::generic_server::*;
+use crate::admin::bucket::*;
use crate::admin::cluster::*;
use crate::admin::key::*;
use crate::admin::router::{Authorization, Endpoint};
@@ -139,12 +140,15 @@ impl ApiHandler for AdminApiServer {
Endpoint::CreateKey => handle_create_key(&self.garage, req).await,
Endpoint::UpdateKey { id } => handle_update_key(&self.garage, id, req).await,
Endpoint::DeleteKey { id } => handle_delete_key(&self.garage, id).await,
- /*
+ // Buckets
+ Endpoint::ListBuckets => handle_list_buckets(&self.garage).await,
+ Endpoint::GetBucketInfo { id, global_alias } => {
+ handle_get_bucket_info(&self.garage, id, global_alias).await
+ }
_ => Err(Error::NotImplemented(format!(
"Admin endpoint {} not implemented yet",
endpoint.name()
))),
- */
}
}
}
diff --git a/src/api/admin/bucket.rs b/src/api/admin/bucket.rs
new file mode 100644
index 00000000..003203c1
--- /dev/null
+++ b/src/api/admin/bucket.rs
@@ -0,0 +1,208 @@
+use std::collections::HashMap;
+use std::sync::Arc;
+
+use hyper::{Body, Request, Response, StatusCode};
+use serde::{Deserialize, Serialize};
+
+use garage_util::data::*;
+use garage_util::error::Error as GarageError;
+
+use garage_table::*;
+
+use garage_model::bucket_table::*;
+use garage_model::garage::Garage;
+use garage_model::key_table::*;
+
+use crate::admin::key::KeyBucketPermResult;
+use crate::error::*;
+use crate::helpers::*;
+
+pub async fn handle_list_buckets(garage: &Arc<Garage>) -> Result<Response<Body>, Error> {
+ let buckets = garage
+ .bucket_table
+ .get_range(
+ &EmptyKey,
+ None,
+ Some(DeletedFilter::NotDeleted),
+ 10000,
+ EnumerationOrder::Forward,
+ )
+ .await?;
+
+ let res = buckets
+ .into_iter()
+ .map(|b| {
+ let state = b.state.as_option().unwrap();
+ ListBucketResultItem {
+ id: hex::encode(b.id),
+ global_aliases: state
+ .aliases
+ .items()
+ .iter()
+ .filter(|(_, _, a)| *a)
+ .map(|(n, _, _)| n.to_string())
+ .collect::<Vec<_>>(),
+ local_aliases: state
+ .local_aliases
+ .items()
+ .iter()
+ .filter(|(_, _, a)| *a)
+ .map(|((k, n), _, _)| ListBucketLocalAlias {
+ access_key_id: k.to_string(),
+ alias: n.to_string(),
+ })
+ .collect::<Vec<_>>(),
+ }
+ })
+ .collect::<Vec<_>>();
+
+ let resp_json = serde_json::to_string_pretty(&res).map_err(GarageError::from)?;
+ Ok(Response::builder()
+ .status(StatusCode::OK)
+ .body(Body::from(resp_json))?)
+}
+
+#[derive(Serialize)]
+struct ListBucketResultItem {
+ id: String,
+ #[serde(rename = "globalAliases")]
+ global_aliases: Vec<String>,
+ #[serde(rename = "localAliases")]
+ local_aliases: Vec<ListBucketLocalAlias>,
+}
+
+#[derive(Serialize)]
+struct ListBucketLocalAlias {
+ #[serde(rename = "accessKeyId")]
+ access_key_id: String,
+ alias: String,
+}
+
+pub async fn handle_get_bucket_info(
+ garage: &Arc<Garage>,
+ id: Option<String>,
+ global_alias: Option<String>,
+) -> Result<Response<Body>, Error> {
+ let bucket_id = match (id, global_alias) {
+ (Some(id), None) => {
+ let id_hex = hex::decode(&id).ok_or_bad_request("Invalid bucket id")?;
+ Uuid::try_from(&id_hex).ok_or_bad_request("Invalid bucket id")?
+ }
+ (None, Some(ga)) => garage
+ .bucket_helper()
+ .resolve_global_bucket_name(&ga)
+ .await?
+ .ok_or_bad_request("Bucket not found")?,
+ _ => {
+ return Err(Error::BadRequest(
+ "Either id or globalAlias must be provided (but not both)".into(),
+ ))
+ }
+ };
+
+ let bucket = garage
+ .bucket_helper()
+ .get_existing_bucket(bucket_id)
+ .await?;
+
+ let mut relevant_keys = HashMap::new();
+ for (k, _) in bucket
+ .state
+ .as_option()
+ .unwrap()
+ .authorized_keys
+ .items()
+ .iter()
+ {
+ if let Some(key) = garage
+ .key_table
+ .get(&EmptyKey, k)
+ .await?
+ .filter(|k| !k.is_deleted())
+ {
+ if !key.state.is_deleted() {
+ relevant_keys.insert(k.clone(), key);
+ }
+ }
+ }
+ for ((k, _), _, _) in bucket
+ .state
+ .as_option()
+ .unwrap()
+ .local_aliases
+ .items()
+ .iter()
+ {
+ if relevant_keys.contains_key(k) {
+ continue;
+ }
+ if let Some(key) = garage.key_table.get(&EmptyKey, k).await? {
+ if !key.state.is_deleted() {
+ relevant_keys.insert(k.clone(), key);
+ }
+ }
+ }
+
+ let state = bucket.state.as_option().unwrap();
+
+ let res = GetBucketInfoResult {
+ id: hex::encode(&bucket.id),
+ global_aliases: state
+ .aliases
+ .items()
+ .iter()
+ .filter(|(_, _, a)| *a)
+ .map(|(n, _, _)| n.to_string())
+ .collect::<Vec<_>>(),
+ keys: relevant_keys
+ .into_iter()
+ .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| KeyBucketPermResult {
+ 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<_>>(),
+ };
+
+ let resp_json = serde_json::to_string_pretty(&res).map_err(GarageError::from)?;
+ Ok(Response::builder()
+ .status(StatusCode::OK)
+ .body(Body::from(resp_json))?)
+}
+
+#[derive(Serialize)]
+struct GetBucketInfoResult {
+ id: String,
+ #[serde(rename = "globalAliases")]
+ global_aliases: Vec<String>,
+ keys: Vec<GetBucketInfoKey>,
+}
+
+#[derive(Serialize)]
+struct GetBucketInfoKey {
+ #[serde(rename = "accessKeyId")]
+ access_key_id: String,
+ #[serde(rename = "name")]
+ name: String,
+ permissions: KeyBucketPermResult,
+ #[serde(rename = "bucketLocalAliases")]
+ bucket_local_aliases: Vec<String>,
+}
diff --git a/src/api/admin/key.rs b/src/api/admin/key.rs
index 7cfe3fce..1252d2c8 100644
--- a/src/api/admin/key.rs
+++ b/src/api/admin/key.rs
@@ -203,11 +203,7 @@ async fn key_info_results(garage: &Arc<Garage>, key: Key) -> Result<Response<Bod
write: p.allow_write,
owner: p.allow_owner,
})
- .unwrap_or(KeyBucketPermResult {
- read: false,
- write: false,
- owner: false,
- }),
+ .unwrap_or_default(),
}
})
.collect::<Vec<_>>(),
@@ -246,9 +242,9 @@ struct KeyInfoBucketResult {
permissions: KeyBucketPermResult,
}
-#[derive(Serialize)]
-struct KeyBucketPermResult {
- read: bool,
- write: bool,
- owner: bool,
+#[derive(Serialize, Default)]
+pub(crate) struct KeyBucketPermResult {
+ pub(crate) read: bool,
+ pub(crate) write: bool,
+ pub(crate) owner: bool,
}
diff --git a/src/api/admin/mod.rs b/src/api/admin/mod.rs
index f6c3b2ee..05097c8b 100644
--- a/src/api/admin/mod.rs
+++ b/src/api/admin/mod.rs
@@ -1,5 +1,6 @@
pub mod api_server;
mod router;
+mod bucket;
mod cluster;
mod key;
diff --git a/src/api/admin/router.rs b/src/api/admin/router.rs
index 626cced1..a6e1c848 100644
--- a/src/api/admin/router.rs
+++ b/src/api/admin/router.rs
@@ -18,10 +18,12 @@ pub enum Endpoint {
Options,
Metrics,
GetClusterStatus,
+ // Layout
GetClusterLayout,
UpdateClusterLayout,
ApplyClusterLayout,
RevertClusterLayout,
+ // Keys
ListKeys,
CreateKey,
GetKeyInfo {
@@ -34,6 +36,16 @@ pub enum Endpoint {
UpdateKey {
id: String,
},
+ // Buckets
+ ListBuckets,
+ CreateBucket,
+ GetBucketInfo {
+ id: Option<String>,
+ global_alias: Option<String>,
+ },
+ DeleteBucket {
+ id: String,
+ },
}}
impl Endpoint {
@@ -63,6 +75,12 @@ impl Endpoint {
POST "/key" => CreateKey,
DELETE "/key" if id => DeleteKey (query::id),
GET "/key" => ListKeys,
+ // Bucket endpoints
+ GET "/bucket" if id => GetBucketInfo (query_opt::id, query_opt::global_alias),
+ GET "/bucket" if global_alias => GetBucketInfo (query_opt::id, query_opt::global_alias),
+ GET "/bucket" => ListBuckets,
+ POST "/bucket" => CreateBucket,
+ DELETE "/bucket" if id => DeleteBucket (query::id),
]);
if let Some(message) = query.nonempty_message() {
@@ -82,5 +100,6 @@ impl Endpoint {
generateQueryParameters! {
"id" => id,
- "search" => search
+ "search" => search,
+ "globalAlias" => global_alias
}