Alex
382e74c798
All checks were successful
continuous-integration/drone/push Build is passing
**Spec:** - [x] Start writing - [x] Specify all layout endpoints - [x] Specify all endpoints for operations on keys - [x] Specify all endpoints for operations on key/bucket permissions - [x] Specify all endpoints for operations on buckets - [x] Specify all endpoints for operations on bucket aliases View rendered spec at <https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/admin-api/doc/drafts/admin-api.md> **Code:** - [x] Refactor code for admin api to use common api code that was created for K2V **General endpoints:** - [x] Metrics - [x] GetClusterStatus - [x] ConnectClusterNodes - [x] GetClusterLayout - [x] UpdateClusterLayout - [x] ApplyClusterLayout - [x] RevertClusterLayout **Key-related endpoints:** - [x] ListKeys - [x] CreateKey - [x] ImportKey - [x] GetKeyInfo - [x] UpdateKey - [x] DeleteKey **Bucket-related endpoints:** - [x] ListBuckets - [x] CreateBucket - [x] GetBucketInfo - [x] DeleteBucket - [x] PutBucketWebsite - [x] DeleteBucketWebsite **Operations on key/bucket permissions:** - [x] BucketAllowKey - [x] BucketDenyKey **Operations on bucket aliases:** - [x] GlobalAliasBucket - [x] GlobalUnaliasBucket - [x] LocalAliasBucket - [x] LocalUnaliasBucket **And also:** - [x] Separate error type for the admin API (this PR includes a quite big refactoring of error handling) - [x] Add management of website access - [ ] Check that nothing is missing wrt what can be done using the CLI - [ ] Improve formatting of the spec - [x] Make sure everyone is cool with the API design Fix #231 Fix #295 Co-authored-by: Alex Auvolat <alex@adnab.me> Reviewed-on: #298 Co-authored-by: Alex <alex@adnab.me> Co-committed-by: Alex <alex@adnab.me>
363 lines
7.8 KiB
Rust
363 lines
7.8 KiB
Rust
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::{EnumerationOrder, TableSchema};
|
|
|
|
use garage_model::garage::Garage;
|
|
use garage_model::k2v::causality::*;
|
|
use garage_model::k2v::item_table::*;
|
|
|
|
use crate::helpers::*;
|
|
use crate::k2v::error::*;
|
|
use crate::k2v::range::read_range;
|
|
|
|
pub async fn handle_insert_batch(
|
|
garage: Arc<Garage>,
|
|
bucket_id: Uuid,
|
|
req: Request<Body>,
|
|
) -> Result<Response<Body>, Error> {
|
|
let items = parse_json_body::<Vec<InsertBatchItem>>(req).await?;
|
|
|
|
let mut items2 = vec![];
|
|
for it in items {
|
|
let ct = it
|
|
.ct
|
|
.map(|s| CausalContext::parse(&s))
|
|
.transpose()
|
|
.ok_or_bad_request("Invalid causality token")?;
|
|
let v = match it.v {
|
|
Some(vs) => {
|
|
DvvsValue::Value(base64::decode(vs).ok_or_bad_request("Invalid base64 value")?)
|
|
}
|
|
None => DvvsValue::Deleted,
|
|
};
|
|
items2.push((it.pk, it.sk, ct, v));
|
|
}
|
|
|
|
garage.k2v.rpc.insert_batch(bucket_id, items2).await?;
|
|
|
|
Ok(Response::builder()
|
|
.status(StatusCode::OK)
|
|
.body(Body::empty())?)
|
|
}
|
|
|
|
pub async fn handle_read_batch(
|
|
garage: Arc<Garage>,
|
|
bucket_id: Uuid,
|
|
req: Request<Body>,
|
|
) -> Result<Response<Body>, Error> {
|
|
let queries = parse_json_body::<Vec<ReadBatchQuery>>(req).await?;
|
|
|
|
let resp_results = futures::future::join_all(
|
|
queries
|
|
.into_iter()
|
|
.map(|q| handle_read_batch_query(&garage, bucket_id, q)),
|
|
)
|
|
.await;
|
|
|
|
let mut resps: Vec<ReadBatchResponse> = vec![];
|
|
for resp in resp_results {
|
|
resps.push(resp?);
|
|
}
|
|
|
|
let resp_json = serde_json::to_string_pretty(&resps).map_err(GarageError::from)?;
|
|
Ok(Response::builder()
|
|
.status(StatusCode::OK)
|
|
.body(Body::from(resp_json))?)
|
|
}
|
|
|
|
async fn handle_read_batch_query(
|
|
garage: &Arc<Garage>,
|
|
bucket_id: Uuid,
|
|
query: ReadBatchQuery,
|
|
) -> Result<ReadBatchResponse, Error> {
|
|
let partition = K2VItemPartition {
|
|
bucket_id,
|
|
partition_key: query.partition_key.clone(),
|
|
};
|
|
|
|
let filter = ItemFilter {
|
|
exclude_only_tombstones: !query.tombstones,
|
|
conflicts_only: query.conflicts_only,
|
|
};
|
|
|
|
let (items, more, next_start) = if query.single_item {
|
|
if query.prefix.is_some() || query.end.is_some() || query.limit.is_some() || query.reverse {
|
|
return Err(Error::bad_request("Batch query parameters 'prefix', 'end', 'limit' and 'reverse' must not be set when singleItem is true."));
|
|
}
|
|
let sk = query
|
|
.start
|
|
.as_ref()
|
|
.ok_or_bad_request("start should be specified if single_item is set")?;
|
|
let item = garage
|
|
.k2v
|
|
.item_table
|
|
.get(&partition, sk)
|
|
.await?
|
|
.filter(|e| K2VItemTable::matches_filter(e, &filter));
|
|
match item {
|
|
Some(i) => (vec![ReadBatchResponseItem::from(i)], false, None),
|
|
None => (vec![], false, None),
|
|
}
|
|
} else {
|
|
let (items, more, next_start) = read_range(
|
|
&garage.k2v.item_table,
|
|
&partition,
|
|
&query.prefix,
|
|
&query.start,
|
|
&query.end,
|
|
query.limit,
|
|
Some(filter),
|
|
EnumerationOrder::from_reverse(query.reverse),
|
|
)
|
|
.await?;
|
|
|
|
let items = items
|
|
.into_iter()
|
|
.map(ReadBatchResponseItem::from)
|
|
.collect::<Vec<_>>();
|
|
|
|
(items, more, next_start)
|
|
};
|
|
|
|
Ok(ReadBatchResponse {
|
|
partition_key: query.partition_key,
|
|
prefix: query.prefix,
|
|
start: query.start,
|
|
end: query.end,
|
|
limit: query.limit,
|
|
reverse: query.reverse,
|
|
single_item: query.single_item,
|
|
conflicts_only: query.conflicts_only,
|
|
tombstones: query.tombstones,
|
|
items,
|
|
more,
|
|
next_start,
|
|
})
|
|
}
|
|
|
|
pub async fn handle_delete_batch(
|
|
garage: Arc<Garage>,
|
|
bucket_id: Uuid,
|
|
req: Request<Body>,
|
|
) -> Result<Response<Body>, Error> {
|
|
let queries = parse_json_body::<Vec<DeleteBatchQuery>>(req).await?;
|
|
|
|
let resp_results = futures::future::join_all(
|
|
queries
|
|
.into_iter()
|
|
.map(|q| handle_delete_batch_query(&garage, bucket_id, q)),
|
|
)
|
|
.await;
|
|
|
|
let mut resps: Vec<DeleteBatchResponse> = vec![];
|
|
for resp in resp_results {
|
|
resps.push(resp?);
|
|
}
|
|
|
|
let resp_json = serde_json::to_string_pretty(&resps).map_err(GarageError::from)?;
|
|
Ok(Response::builder()
|
|
.status(StatusCode::OK)
|
|
.body(Body::from(resp_json))?)
|
|
}
|
|
|
|
async fn handle_delete_batch_query(
|
|
garage: &Arc<Garage>,
|
|
bucket_id: Uuid,
|
|
query: DeleteBatchQuery,
|
|
) -> Result<DeleteBatchResponse, Error> {
|
|
let partition = K2VItemPartition {
|
|
bucket_id,
|
|
partition_key: query.partition_key.clone(),
|
|
};
|
|
|
|
let filter = ItemFilter {
|
|
exclude_only_tombstones: true,
|
|
conflicts_only: false,
|
|
};
|
|
|
|
let deleted_items = if query.single_item {
|
|
if query.prefix.is_some() || query.end.is_some() {
|
|
return Err(Error::bad_request("Batch query parameters 'prefix' and 'end' must not be set when singleItem is true."));
|
|
}
|
|
let sk = query
|
|
.start
|
|
.as_ref()
|
|
.ok_or_bad_request("start should be specified if single_item is set")?;
|
|
let item = garage
|
|
.k2v
|
|
.item_table
|
|
.get(&partition, sk)
|
|
.await?
|
|
.filter(|e| K2VItemTable::matches_filter(e, &filter));
|
|
match item {
|
|
Some(i) => {
|
|
let cc = i.causal_context();
|
|
garage
|
|
.k2v
|
|
.rpc
|
|
.insert(
|
|
bucket_id,
|
|
i.partition.partition_key,
|
|
i.sort_key,
|
|
Some(cc),
|
|
DvvsValue::Deleted,
|
|
)
|
|
.await?;
|
|
1
|
|
}
|
|
None => 0,
|
|
}
|
|
} else {
|
|
let (items, more, _next_start) = read_range(
|
|
&garage.k2v.item_table,
|
|
&partition,
|
|
&query.prefix,
|
|
&query.start,
|
|
&query.end,
|
|
None,
|
|
Some(filter),
|
|
EnumerationOrder::Forward,
|
|
)
|
|
.await?;
|
|
assert!(!more);
|
|
|
|
// TODO delete items
|
|
let items = items
|
|
.into_iter()
|
|
.map(|i| {
|
|
let cc = i.causal_context();
|
|
(
|
|
i.partition.partition_key,
|
|
i.sort_key,
|
|
Some(cc),
|
|
DvvsValue::Deleted,
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let n = items.len();
|
|
|
|
garage.k2v.rpc.insert_batch(bucket_id, items).await?;
|
|
|
|
n
|
|
};
|
|
|
|
Ok(DeleteBatchResponse {
|
|
partition_key: query.partition_key,
|
|
prefix: query.prefix,
|
|
start: query.start,
|
|
end: query.end,
|
|
single_item: query.single_item,
|
|
deleted_items,
|
|
})
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct InsertBatchItem {
|
|
pk: String,
|
|
sk: String,
|
|
ct: Option<String>,
|
|
v: Option<String>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct ReadBatchQuery {
|
|
#[serde(rename = "partitionKey")]
|
|
partition_key: String,
|
|
#[serde(default)]
|
|
prefix: Option<String>,
|
|
#[serde(default)]
|
|
start: Option<String>,
|
|
#[serde(default)]
|
|
end: Option<String>,
|
|
#[serde(default)]
|
|
limit: Option<u64>,
|
|
#[serde(default)]
|
|
reverse: bool,
|
|
#[serde(default, rename = "singleItem")]
|
|
single_item: bool,
|
|
#[serde(default, rename = "conflictsOnly")]
|
|
conflicts_only: bool,
|
|
#[serde(default)]
|
|
tombstones: bool,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct ReadBatchResponse {
|
|
#[serde(rename = "partitionKey")]
|
|
partition_key: String,
|
|
prefix: Option<String>,
|
|
start: Option<String>,
|
|
end: Option<String>,
|
|
limit: Option<u64>,
|
|
reverse: bool,
|
|
#[serde(rename = "singleItem")]
|
|
single_item: bool,
|
|
#[serde(rename = "conflictsOnly")]
|
|
conflicts_only: bool,
|
|
tombstones: bool,
|
|
|
|
items: Vec<ReadBatchResponseItem>,
|
|
more: bool,
|
|
#[serde(rename = "nextStart")]
|
|
next_start: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct ReadBatchResponseItem {
|
|
sk: String,
|
|
ct: String,
|
|
v: Vec<Option<String>>,
|
|
}
|
|
|
|
impl ReadBatchResponseItem {
|
|
fn from(i: K2VItem) -> Self {
|
|
let ct = i.causal_context().serialize();
|
|
let v = i
|
|
.values()
|
|
.iter()
|
|
.map(|v| match v {
|
|
DvvsValue::Value(x) => Some(base64::encode(x)),
|
|
DvvsValue::Deleted => None,
|
|
})
|
|
.collect::<Vec<_>>();
|
|
Self {
|
|
sk: i.sort_key,
|
|
ct,
|
|
v,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct DeleteBatchQuery {
|
|
#[serde(rename = "partitionKey")]
|
|
partition_key: String,
|
|
#[serde(default)]
|
|
prefix: Option<String>,
|
|
#[serde(default)]
|
|
start: Option<String>,
|
|
#[serde(default)]
|
|
end: Option<String>,
|
|
#[serde(default, rename = "singleItem")]
|
|
single_item: bool,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct DeleteBatchResponse {
|
|
#[serde(rename = "partitionKey")]
|
|
partition_key: String,
|
|
prefix: Option<String>,
|
|
start: Option<String>,
|
|
end: Option<String>,
|
|
#[serde(rename = "singleItem")]
|
|
single_item: bool,
|
|
|
|
#[serde(rename = "deletedItems")]
|
|
deleted_items: usize,
|
|
}
|