Merge pull request 'admin api: implement InspectObject (fix #892)' (#1005) from inspect-object into next-v2
Reviewed-on: #1005
This commit is contained in:
commit
2d1c073d2f
8 changed files with 486 additions and 2 deletions
|
@ -1054,6 +1054,51 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/v2/InspectObject": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Bucket"
|
||||
],
|
||||
"description": "\nReturns detailed information about an object in a bucket, including its internal state in Garage.\n\nThis API call can be used to list the data blocks referenced by an object,\nas well as to view metadata associated to the object.\n\nThis call may return a list of more than one version for the object, for instance in the\ncase where there is a currently stored version of the object, and a newer version whose\nupload is in progress and not yet finished.\n ",
|
||||
"operationId": "InspectObject",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "bucketId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "key",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Returns exhaustive information about the object",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InspectObjectResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Object not found"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v2/LaunchRepairOperation": {
|
||||
"post": {
|
||||
"tags": [
|
||||
|
@ -2571,6 +2616,146 @@
|
|||
"ImportKeyResponse": {
|
||||
"$ref": "#/components/schemas/GetKeyInfoResponse"
|
||||
},
|
||||
"InspectObjectBlock": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"partNumber",
|
||||
"offset",
|
||||
"hash",
|
||||
"size"
|
||||
],
|
||||
"properties": {
|
||||
"hash": {
|
||||
"type": "string",
|
||||
"description": "Hash (blake2 sum) of the block's data"
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Offset of this block within the part",
|
||||
"minimum": 0
|
||||
},
|
||||
"partNumber": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Part number of the part containing this block, for multipart uploads",
|
||||
"minimum": 0
|
||||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Length of the blocks's data",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"InspectObjectResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"bucketId",
|
||||
"key",
|
||||
"versions"
|
||||
],
|
||||
"properties": {
|
||||
"bucketId": {
|
||||
"type": "string",
|
||||
"description": "ID of the bucket containing the inspected object"
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "Key of the inspected object"
|
||||
},
|
||||
"versions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/InspectObjectVersion"
|
||||
},
|
||||
"description": "List of versions currently stored for this object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"InspectObjectVersion": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"uuid",
|
||||
"timestamp",
|
||||
"encrypted",
|
||||
"uploading",
|
||||
"aborted",
|
||||
"deleteMarker",
|
||||
"inline"
|
||||
],
|
||||
"properties": {
|
||||
"aborted": {
|
||||
"type": "boolean",
|
||||
"description": "Whether this is an aborted upload"
|
||||
},
|
||||
"blocks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/InspectObjectBlock"
|
||||
},
|
||||
"description": "List of data blocks for this object version"
|
||||
},
|
||||
"deleteMarker": {
|
||||
"type": "boolean",
|
||||
"description": "Whether this version is a delete marker (a tombstone indicating that a previous version of\nthe object has been deleted)"
|
||||
},
|
||||
"encrypted": {
|
||||
"type": "boolean",
|
||||
"description": "Whether this object version was created with SSE-C encryption"
|
||||
},
|
||||
"etag": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "Etag of this object version"
|
||||
},
|
||||
"headers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": false,
|
||||
"prefixItems": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Metadata (HTTP headers) associated with this object version"
|
||||
},
|
||||
"inline": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the object's data is stored inline (for small objects)"
|
||||
},
|
||||
"size": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "int64",
|
||||
"description": "Size of the object, in bytes",
|
||||
"minimum": 0
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Creation timestamp of this object version"
|
||||
},
|
||||
"uploading": {
|
||||
"type": "boolean",
|
||||
"description": "Whether this object version is still uploading"
|
||||
},
|
||||
"uuid": {
|
||||
"type": "string",
|
||||
"description": "Version ID"
|
||||
}
|
||||
}
|
||||
},
|
||||
"KeyInfoBucketResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
|
|
@ -80,6 +80,7 @@ admin_endpoints![
|
|||
UpdateBucket,
|
||||
DeleteBucket,
|
||||
CleanupIncompleteUploads,
|
||||
InspectObject,
|
||||
|
||||
// Operations on permissions for keys on buckets
|
||||
AllowBucketKey,
|
||||
|
@ -907,6 +908,67 @@ pub struct CleanupIncompleteUploadsResponse {
|
|||
pub uploads_deleted: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, IntoParams)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InspectObjectRequest {
|
||||
pub bucket_id: String,
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InspectObjectResponse {
|
||||
/// ID of the bucket containing the inspected object
|
||||
pub bucket_id: String,
|
||||
/// Key of the inspected object
|
||||
pub key: String,
|
||||
/// List of versions currently stored for this object
|
||||
pub versions: Vec<InspectObjectVersion>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InspectObjectVersion {
|
||||
/// Version ID
|
||||
pub uuid: String,
|
||||
/// Creation timestamp of this object version
|
||||
pub timestamp: chrono::DateTime<chrono::Utc>,
|
||||
/// Whether this object version was created with SSE-C encryption
|
||||
pub encrypted: bool,
|
||||
/// Whether this object version is still uploading
|
||||
pub uploading: bool,
|
||||
/// Whether this is an aborted upload
|
||||
pub aborted: bool,
|
||||
/// Whether this version is a delete marker (a tombstone indicating that a previous version of
|
||||
/// the object has been deleted)
|
||||
pub delete_marker: bool,
|
||||
/// Whether the object's data is stored inline (for small objects)
|
||||
pub inline: bool,
|
||||
/// Size of the object, in bytes
|
||||
pub size: Option<u64>,
|
||||
/// Etag of this object version
|
||||
pub etag: Option<String>,
|
||||
/// Metadata (HTTP headers) associated with this object version
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub headers: Vec<(String, String)>,
|
||||
/// List of data blocks for this object version
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub blocks: Vec<InspectObjectBlock>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InspectObjectBlock {
|
||||
/// Part number of the part containing this block, for multipart uploads
|
||||
pub part_number: u64,
|
||||
/// Offset of this block within the part
|
||||
pub offset: u64,
|
||||
/// Hash (blake2 sum) of the block's data
|
||||
pub hash: String,
|
||||
/// Length of the blocks's data
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
// **********************************************
|
||||
// Operations on permissions for keys on buckets
|
||||
// **********************************************
|
||||
|
|
|
@ -2,6 +2,8 @@ use std::collections::HashMap;
|
|||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::DateTime;
|
||||
|
||||
use garage_util::crdt::*;
|
||||
use garage_util::data::*;
|
||||
use garage_util::time::*;
|
||||
|
@ -349,6 +351,127 @@ impl RequestHandler for CleanupIncompleteUploadsRequest {
|
|||
}
|
||||
}
|
||||
|
||||
impl RequestHandler for InspectObjectRequest {
|
||||
type Response = InspectObjectResponse;
|
||||
|
||||
async fn handle(
|
||||
self,
|
||||
garage: &Arc<Garage>,
|
||||
_admin: &Admin,
|
||||
) -> Result<InspectObjectResponse, Error> {
|
||||
let bucket_id = parse_bucket_id(&self.bucket_id)?;
|
||||
|
||||
let object = garage
|
||||
.object_table
|
||||
.get(&bucket_id, &self.key)
|
||||
.await?
|
||||
.ok_or_else(|| Error::NoSuchKey)?;
|
||||
|
||||
let mut versions = vec![];
|
||||
for obj_ver in object.versions().iter() {
|
||||
let ver = garage.version_table.get(&obj_ver.uuid, &EmptyKey).await?;
|
||||
let blocks = ver
|
||||
.map(|v| {
|
||||
v.blocks
|
||||
.items()
|
||||
.iter()
|
||||
.map(|(vk, vb)| InspectObjectBlock {
|
||||
part_number: vk.part_number,
|
||||
offset: vk.offset,
|
||||
hash: hex::encode(&vb.hash),
|
||||
size: vb.size,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let uuid = hex::encode(&obj_ver.uuid);
|
||||
let timestamp = DateTime::from_timestamp_millis(obj_ver.timestamp as i64)
|
||||
.expect("invalid timestamp in db");
|
||||
match &obj_ver.state {
|
||||
ObjectVersionState::Uploading { encryption, .. } => {
|
||||
versions.push(InspectObjectVersion {
|
||||
uuid,
|
||||
timestamp,
|
||||
encrypted: !matches!(encryption, ObjectVersionEncryption::Plaintext { .. }),
|
||||
uploading: true,
|
||||
headers: match encryption {
|
||||
ObjectVersionEncryption::Plaintext { inner } => inner.headers.clone(),
|
||||
_ => vec![],
|
||||
},
|
||||
blocks,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
ObjectVersionState::Complete(data) => match data {
|
||||
ObjectVersionData::DeleteMarker => {
|
||||
versions.push(InspectObjectVersion {
|
||||
uuid,
|
||||
timestamp,
|
||||
delete_marker: true,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
ObjectVersionData::Inline(meta, _) => {
|
||||
versions.push(InspectObjectVersion {
|
||||
uuid,
|
||||
timestamp,
|
||||
inline: true,
|
||||
size: Some(meta.size),
|
||||
etag: Some(meta.etag.clone()),
|
||||
encrypted: !matches!(
|
||||
meta.encryption,
|
||||
ObjectVersionEncryption::Plaintext { .. }
|
||||
),
|
||||
headers: match &meta.encryption {
|
||||
ObjectVersionEncryption::Plaintext { inner } => {
|
||||
inner.headers.clone()
|
||||
}
|
||||
_ => vec![],
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
ObjectVersionData::FirstBlock(meta, _) => {
|
||||
versions.push(InspectObjectVersion {
|
||||
uuid,
|
||||
timestamp,
|
||||
size: Some(meta.size),
|
||||
etag: Some(meta.etag.clone()),
|
||||
encrypted: !matches!(
|
||||
meta.encryption,
|
||||
ObjectVersionEncryption::Plaintext { .. }
|
||||
),
|
||||
headers: match &meta.encryption {
|
||||
ObjectVersionEncryption::Plaintext { inner } => {
|
||||
inner.headers.clone()
|
||||
}
|
||||
_ => vec![],
|
||||
},
|
||||
blocks,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
},
|
||||
ObjectVersionState::Aborted => {
|
||||
versions.push(InspectObjectVersion {
|
||||
uuid,
|
||||
timestamp,
|
||||
aborted: true,
|
||||
blocks,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(InspectObjectResponse {
|
||||
bucket_id: hex::encode(&object.bucket_id),
|
||||
key: object.key,
|
||||
versions,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---- BUCKET/KEY PERMISSIONS ----
|
||||
|
||||
impl RequestHandler for AllowBucketKeyRequest {
|
||||
|
|
|
@ -37,6 +37,10 @@ pub enum Error {
|
|||
#[error(display = "Worker not found: {}", _0)]
|
||||
NoSuchWorker(u64),
|
||||
|
||||
/// The object requested don't exists
|
||||
#[error(display = "Key not found")]
|
||||
NoSuchKey,
|
||||
|
||||
/// In Import key, the key already exists
|
||||
#[error(
|
||||
display = "Key {} already exists in data store. Even if it is deleted, we can't let you create a new key with the same ID. Sorry.",
|
||||
|
@ -69,6 +73,7 @@ impl Error {
|
|||
Error::NoSuchWorker(_) => "NoSuchWorker",
|
||||
Error::NoSuchBlock(_) => "NoSuchBlock",
|
||||
Error::KeyAlreadyExists(_) => "KeyAlreadyExists",
|
||||
Error::NoSuchKey => "NoSuchKey",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -81,7 +86,8 @@ impl ApiError for Error {
|
|||
Error::NoSuchAdminToken(_)
|
||||
| Error::NoSuchAccessKey(_)
|
||||
| Error::NoSuchWorker(_)
|
||||
| Error::NoSuchBlock(_) => StatusCode::NOT_FOUND,
|
||||
| Error::NoSuchBlock(_)
|
||||
| Error::NoSuchKey => StatusCode::NOT_FOUND,
|
||||
Error::KeyAlreadyExists(_) => StatusCode::CONFLICT,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -509,6 +509,28 @@ fn DeleteBucket() -> () {}
|
|||
)]
|
||||
fn CleanupIncompleteUploads() -> () {}
|
||||
|
||||
#[utoipa::path(get,
|
||||
path = "/v2/InspectObject",
|
||||
tag = "Bucket",
|
||||
description = "
|
||||
Returns detailed information about an object in a bucket, including its internal state in Garage.
|
||||
|
||||
This API call can be used to list the data blocks referenced by an object,
|
||||
as well as to view metadata associated to the object.
|
||||
|
||||
This call may return a list of more than one version for the object, for instance in the
|
||||
case where there is a currently stored version of the object, and a newer version whose
|
||||
upload is in progress and not yet finished.
|
||||
",
|
||||
params(InspectObjectRequest),
|
||||
responses(
|
||||
(status = 200, description = "Returns exhaustive information about the object", body = InspectObjectResponse),
|
||||
(status = 404, description = "Object not found"),
|
||||
(status = 500, description = "Internal server error")
|
||||
),
|
||||
)]
|
||||
fn InspectObject() -> () {}
|
||||
|
||||
// **********************************************
|
||||
// Operations on permissions for keys on buckets
|
||||
// **********************************************
|
||||
|
@ -872,6 +894,7 @@ impl Modify for SecurityAddon {
|
|||
UpdateBucket,
|
||||
DeleteBucket,
|
||||
CleanupIncompleteUploads,
|
||||
InspectObject,
|
||||
// Operations on permissions
|
||||
AllowBucketKey,
|
||||
DenyBucketKey,
|
||||
|
|
|
@ -62,6 +62,7 @@ impl AdminApiRequest {
|
|||
POST DeleteBucket (query::id),
|
||||
POST UpdateBucket (body_field, query::id),
|
||||
POST CleanupIncompleteUploads (body),
|
||||
GET InspectObject (query::bucket_id, query::key),
|
||||
// Bucket-key permissions
|
||||
POST AllowBucketKey (body),
|
||||
POST DenyBucketKey (body),
|
||||
|
@ -267,6 +268,8 @@ generateQueryParameters! {
|
|||
"globalAlias" => global_alias,
|
||||
"alias" => alias,
|
||||
"accessKeyId" => access_key_id,
|
||||
"showSecretKey" => show_secret_key
|
||||
"showSecretKey" => show_secret_key,
|
||||
"bucketId" => bucket_id,
|
||||
"key" => key
|
||||
]
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ impl Cli {
|
|||
BucketOperation::CleanupIncompleteUploads(query) => {
|
||||
self.cmd_cleanup_incomplete_uploads(query).await
|
||||
}
|
||||
BucketOperation::InspectObject(query) => self.cmd_inspect_object(query).await,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -407,6 +408,75 @@ impl Cli {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn cmd_inspect_object(&self, opt: InspectObjectOpt) -> Result<(), Error> {
|
||||
let bucket = self
|
||||
.api_request(GetBucketInfoRequest {
|
||||
id: None,
|
||||
global_alias: None,
|
||||
search: Some(opt.bucket),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let info = self
|
||||
.api_request(InspectObjectRequest {
|
||||
bucket_id: bucket.id,
|
||||
key: opt.key,
|
||||
})
|
||||
.await?;
|
||||
|
||||
for ver in info.versions {
|
||||
println!("==== OBJECT VERSION ====");
|
||||
let mut tab = vec![
|
||||
format!("Bucket ID:\t{}", info.bucket_id),
|
||||
format!("Key:\t{}", info.key),
|
||||
format!("Version ID:\t{}", ver.uuid),
|
||||
format!("Timestamp:\t{}", ver.timestamp),
|
||||
];
|
||||
if let Some(size) = ver.size {
|
||||
let bs = bytesize::ByteSize::b(size);
|
||||
tab.push(format!(
|
||||
"Size:\t{} ({})",
|
||||
bs.to_string_as(true),
|
||||
bs.to_string_as(false)
|
||||
));
|
||||
tab.push(format!("Size (exact):\t{}", size));
|
||||
if !ver.blocks.is_empty() {
|
||||
tab.push(format!("Number of blocks:\t{:?}", ver.blocks.len()));
|
||||
}
|
||||
}
|
||||
if let Some(etag) = ver.etag {
|
||||
tab.push(format!("Etag:\t{}", etag));
|
||||
}
|
||||
tab.extend([
|
||||
format!("Encrypted:\t{}", ver.encrypted),
|
||||
format!("Uploading:\t{}", ver.uploading),
|
||||
format!("Aborted:\t{}", ver.aborted),
|
||||
format!("Delete marker:\t{}", ver.delete_marker),
|
||||
format!("Inline data:\t{}", ver.inline),
|
||||
]);
|
||||
if !ver.headers.is_empty() {
|
||||
tab.push(String::new());
|
||||
tab.extend(ver.headers.iter().map(|(k, v)| format!("{}\t{}", k, v)));
|
||||
}
|
||||
format_table(tab);
|
||||
|
||||
if !ver.blocks.is_empty() {
|
||||
let mut tab = vec!["Part#\tOffset\tBlock hash\tSize".to_string()];
|
||||
tab.extend(ver.blocks.iter().map(|b| {
|
||||
format!(
|
||||
"{:4}\t{:9}\t{}\t{:9}",
|
||||
b.part_number, b.offset, b.hash, b.size
|
||||
)
|
||||
}));
|
||||
println!();
|
||||
format_table(tab);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn print_bucket_info(bucket: &GetBucketInfoResponse) {
|
||||
|
|
|
@ -265,6 +265,10 @@ pub enum BucketOperation {
|
|||
/// Clean up (abort) old incomplete multipart uploads
|
||||
#[structopt(name = "cleanup-incomplete-uploads", version = garage_version())]
|
||||
CleanupIncompleteUploads(CleanupIncompleteUploadsOpt),
|
||||
|
||||
/// Inspect an object in a bucket
|
||||
#[structopt(name = "inspect-object", version = garage_version())]
|
||||
InspectObject(InspectObjectOpt),
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
|
@ -377,6 +381,14 @@ pub struct CleanupIncompleteUploadsOpt {
|
|||
pub buckets: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
pub struct InspectObjectOpt {
|
||||
/// Name or ID of bucket
|
||||
pub bucket: String,
|
||||
/// Key of object to inspect
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
// ------------------------
|
||||
// ---- garage key ... ----
|
||||
// ------------------------
|
||||
|
|
Loading…
Add table
Reference in a new issue