admin api: update semantics of some endpoints, and update doc
Some checks failed
ci/woodpecker/push/debug Pipeline failed
ci/woodpecker/pr/debug Pipeline failed

This commit is contained in:
Alex 2025-01-28 16:18:48 +01:00
parent 9beb66a63c
commit 2abfd4fb3a
6 changed files with 122 additions and 58 deletions

View file

@ -13,8 +13,9 @@ We will bump the version numbers prefixed to each API endpoint each time the syn
or semantics change, meaning that code that relies on these endpoints will break or semantics change, meaning that code that relies on these endpoints will break
when changes are introduced. when changes are introduced.
The Garage administration API was introduced in version 0.7.2, this document The Garage administration API was introduced in version 0.7.2, and was
does not apply to older versions of Garage. changed several times.
This document applies only to the Garage v2 API (starting with Garage v2.0.0).
## Access control ## Access control
@ -52,11 +53,18 @@ Returns an HTTP status 200 if the node is ready to answer user's requests,
and an HTTP status 503 (Service Unavailable) if there are some partitions and an HTTP status 503 (Service Unavailable) if there are some partitions
for which a quorum of nodes is not available. for which a quorum of nodes is not available.
A simple textual message is also returned in a body with content-type `text/plain`. A simple textual message is also returned in a body with content-type `text/plain`.
See `/v1/health` for an API that also returns JSON output. See `/v2/health` for an API that also returns JSON output.
### Other special endpoints
#### CheckDomain `GET /check?domain=<domain>`
Checks whether this Garage cluster serves a website for domain `<domain>`.
Returns HTTP 200 Ok if yes, or HTTP 4xx if no website is available for this domain.
### Cluster operations ### Cluster operations
#### GetClusterStatus `GET /v1/status` #### GetClusterStatus `GET /v2/GetClusterStatus`
Returns the cluster's current status in JSON, including: Returns the cluster's current status in JSON, including:
@ -70,7 +78,7 @@ Example response body:
```json ```json
{ {
"node": "b10c110e4e854e5aa3f4637681befac755154b20059ec163254ddbfae86b09df", "node": "b10c110e4e854e5aa3f4637681befac755154b20059ec163254ddbfae86b09df",
"garageVersion": "v1.0.1", "garageVersion": "v2.0.0",
"garageFeatures": [ "garageFeatures": [
"k2v", "k2v",
"lmdb", "lmdb",
@ -169,7 +177,7 @@ Example response body:
} }
``` ```
#### GetClusterHealth `GET /v1/health` #### GetClusterHealth `GET /v2/GetClusterHealth`
Returns the cluster's current health in JSON format, with the following variables: Returns the cluster's current health in JSON format, with the following variables:
@ -202,7 +210,7 @@ Example response body:
} }
``` ```
#### ConnectClusterNodes `POST /v1/connect` #### ConnectClusterNodes `POST /v2/ConnectClusterNodes`
Instructs this Garage node to connect to other Garage nodes at specified addresses. Instructs this Garage node to connect to other Garage nodes at specified addresses.
@ -232,7 +240,7 @@ Example response:
] ]
``` ```
#### GetClusterLayout `GET /v1/layout` #### GetClusterLayout `GET /v2/GetClusterLayout`
Returns the cluster's current layout in JSON, including: Returns the cluster's current layout in JSON, including:
@ -293,7 +301,7 @@ Example response body:
} }
``` ```
#### UpdateClusterLayout `POST /v1/layout` #### UpdateClusterLayout `POST /v2/UpdateClusterLayout`
Send modifications to the cluster layout. These modifications will Send modifications to the cluster layout. These modifications will
be included in the staged role changes, visible in subsequent calls be included in the staged role changes, visible in subsequent calls
@ -330,7 +338,7 @@ This returns the new cluster layout with the proposed staged changes,
as returned by GetClusterLayout. as returned by GetClusterLayout.
#### ApplyClusterLayout `POST /v1/layout/apply` #### ApplyClusterLayout `POST /v2/ApplyClusterLayout`
Applies to the cluster the layout changes currently registered as Applies to the cluster the layout changes currently registered as
staged layout changes. staged layout changes.
@ -350,7 +358,7 @@ existing layout in the cluster.
This returns the message describing all the calculations done to compute the new This returns the message describing all the calculations done to compute the new
layout, as well as the description of the layout as returned by GetClusterLayout. layout, as well as the description of the layout as returned by GetClusterLayout.
#### RevertClusterLayout `POST /v1/layout/revert` #### RevertClusterLayout `POST /v2/RevertClusterLayout`
Clears all of the staged layout changes. Clears all of the staged layout changes.
@ -374,7 +382,7 @@ as returned by GetClusterLayout.
### Access key operations ### Access key operations
#### ListKeys `GET /v1/key` #### ListKeys `GET /v2/ListKeys`
Returns all API access keys in the cluster. Returns all API access keys in the cluster.
@ -393,8 +401,8 @@ Example response:
] ]
``` ```
#### GetKeyInfo `GET /v1/key?id=<acces key id>` #### GetKeyInfo `GET /v2/GetKeyInfo?id=<acces key id>`
#### GetKeyInfo `GET /v1/key?search=<pattern>` #### GetKeyInfo `GET /v2/GetKeyInfo?search=<pattern>`
Returns information about the requested API access key. Returns information about the requested API access key.
@ -468,7 +476,7 @@ Example response:
} }
``` ```
#### CreateKey `POST /v1/key` #### CreateKey `POST /v2/CreateKey`
Creates a new API access key. Creates a new API access key.
@ -483,7 +491,7 @@ Request body format:
This returns the key info, including the created secret key, This returns the key info, including the created secret key,
in the same format as the result of GetKeyInfo. in the same format as the result of GetKeyInfo.
#### ImportKey `POST /v1/key/import` #### ImportKey `POST /v2/ImportKey`
Imports an existing API key. Imports an existing API key.
This will check that the imported key is in the valid format, i.e. This will check that the imported key is in the valid format, i.e.
@ -501,7 +509,7 @@ Request body format:
This returns the key info in the same format as the result of GetKeyInfo. This returns the key info in the same format as the result of GetKeyInfo.
#### UpdateKey `POST /v1/key?id=<acces key id>` #### UpdateKey `POST /v2/UpdateKey?id=<acces key id>`
Updates information about the specified API access key. Updates information about the specified API access key.
@ -523,14 +531,14 @@ The possible flags in `allow` and `deny` are: `createBucket`.
This returns the key info in the same format as the result of GetKeyInfo. This returns the key info in the same format as the result of GetKeyInfo.
#### DeleteKey `DELETE /v1/key?id=<acces key id>` #### DeleteKey `POST /v2/DeleteKey?id=<acces key id>`
Deletes an API access key. Deletes an API access key.
### Bucket operations ### Bucket operations
#### ListBuckets `GET /v1/bucket` #### ListBuckets `GET /v2/ListBuckets`
Returns all storage buckets in the cluster. Returns all storage buckets in the cluster.
@ -572,8 +580,8 @@ Example response:
] ]
``` ```
#### GetBucketInfo `GET /v1/bucket?id=<bucket id>` #### GetBucketInfo `GET /v2/GetBucketInfo?id=<bucket id>`
#### GetBucketInfo `GET /v1/bucket?globalAlias=<alias>` #### GetBucketInfo `GET /v2/GetBucketInfo?globalAlias=<alias>`
Returns information about the requested storage bucket. Returns information about the requested storage bucket.
@ -616,7 +624,7 @@ Example response:
} }
``` ```
#### CreateBucket `POST /v1/bucket` #### CreateBucket `POST /v2/CreateBucket`
Creates a new storage bucket. Creates a new storage bucket.
@ -656,7 +664,7 @@ or no alias at all.
Technically, you can also specify both `globalAlias` and `localAlias` and that would create Technically, you can also specify both `globalAlias` and `localAlias` and that would create
two aliases, but I don't see why you would want to do that. two aliases, but I don't see why you would want to do that.
#### UpdateBucket `PUT /v1/bucket?id=<bucket id>` #### UpdateBucket `POST /v2/UpdateBucket?id=<bucket id>`
Updates configuration of the given bucket. Updates configuration of the given bucket.
@ -688,7 +696,7 @@ In `quotas`: new values of `maxSize` and `maxObjects` must both be specified, or
to remove the quotas. An absent value will be considered the same as a `null`. It is not possible to remove the quotas. An absent value will be considered the same as a `null`. It is not possible
to change only one of the two quotas. to change only one of the two quotas.
#### DeleteBucket `DELETE /v1/bucket?id=<bucket id>` #### DeleteBucket `POST /v2/DeleteBucket?id=<bucket id>`
Deletes a storage bucket. A bucket cannot be deleted if it is not empty. Deletes a storage bucket. A bucket cannot be deleted if it is not empty.
@ -697,7 +705,7 @@ Warning: this will delete all aliases associated with the bucket!
### Operations on permissions for keys on buckets ### Operations on permissions for keys on buckets
#### BucketAllowKey `POST /v1/bucket/allow` #### BucketAllowKey `POST /v2/BucketAllowKey`
Allows a key to do read/write/owner operations on a bucket. Allows a key to do read/write/owner operations on a bucket.
@ -718,7 +726,7 @@ Request body format:
Flags in `permissions` which have the value `true` will be activated. Flags in `permissions` which have the value `true` will be activated.
Other flags will remain unchanged. Other flags will remain unchanged.
#### BucketDenyKey `POST /v1/bucket/deny` #### BucketDenyKey `POST /v2/BucketDenyKey`
Denies a key from doing read/write/owner operations on a bucket. Denies a key from doing read/write/owner operations on a bucket.
@ -742,19 +750,57 @@ Other flags will remain unchanged.
### Operations on bucket aliases ### Operations on bucket aliases
#### GlobalAliasBucket `PUT /v1/bucket/alias/global?id=<bucket id>&alias=<global alias>` #### GlobalAliasBucket `POST /v2/GlobalAliasBucket`
Empty body. Creates a global alias for a bucket. Creates a global alias for a bucket.
#### GlobalUnaliasBucket `DELETE /v1/bucket/alias/global?id=<bucket id>&alias=<global alias>` Request body format:
```json
{
"bucketId": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b",
"alias": "the-bucket"
}
```
#### GlobalUnaliasBucket `POST /v2/GlobalUnaliasBucket`
Removes a global alias for a bucket. Removes a global alias for a bucket.
#### LocalAliasBucket `PUT /v1/bucket/alias/local?id=<bucket id>&accessKeyId=<access key ID>&alias=<local alias>` Request body format:
Empty body. Creates a local alias for a bucket in the namespace of a specific access key. ```json
{
"bucketId": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b",
"alias": "the-bucket"
}
```
#### LocalUnaliasBucket `DELETE /v1/bucket/alias/local?id=<bucket id>&accessKeyId<access key ID>&alias=<local alias>` #### LocalAliasBucket `POST /v2/LocalAliasBucket`
Creates a local alias for a bucket in the namespace of a specific access key.
Request body format:
```json
{
"bucketId": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b",
"accessKeyId": "GK31c2f218a2e44f485b94239e",
"alias": "my-bucket"
}
```
#### LocalUnaliasBucket `POST /v2/LocalUnaliasBucket`
Removes a local alias for a bucket in the namespace of a specific access key. Removes a local alias for a bucket in the namespace of a specific access key.
Request body format:
```json
{
"bucketId": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b",
"accessKeyId": "GK31c2f218a2e44f485b94239e",
"alias": "my-bucket"
}
```

View file

@ -500,8 +500,9 @@ pub struct BucketDenyKeyResponse(pub GetBucketInfoResponse);
// ---- GlobalAliasBucket ---- // ---- GlobalAliasBucket ----
#[derive(Deserialize)]
pub struct GlobalAliasBucketRequest { pub struct GlobalAliasBucketRequest {
pub id: String, pub bucket_id: String,
pub alias: String, pub alias: String,
} }
@ -510,8 +511,9 @@ pub struct GlobalAliasBucketResponse(pub GetBucketInfoResponse);
// ---- GlobalUnaliasBucket ---- // ---- GlobalUnaliasBucket ----
#[derive(Deserialize)]
pub struct GlobalUnaliasBucketRequest { pub struct GlobalUnaliasBucketRequest {
pub id: String, pub bucket_id: String,
pub alias: String, pub alias: String,
} }
@ -520,8 +522,9 @@ pub struct GlobalUnaliasBucketResponse(pub GetBucketInfoResponse);
// ---- LocalAliasBucket ---- // ---- LocalAliasBucket ----
#[derive(Deserialize)]
pub struct LocalAliasBucketRequest { pub struct LocalAliasBucketRequest {
pub id: String, pub bucket_id: String,
pub access_key_id: String, pub access_key_id: String,
pub alias: String, pub alias: String,
} }
@ -531,8 +534,9 @@ pub struct LocalAliasBucketResponse(pub GetBucketInfoResponse);
// ---- LocalUnaliasBucket ---- // ---- LocalUnaliasBucket ----
#[derive(Deserialize)]
pub struct LocalUnaliasBucketRequest { pub struct LocalUnaliasBucketRequest {
pub id: String, pub bucket_id: String,
pub access_key_id: String, pub access_key_id: String,
pub alias: String, pub alias: String,
} }

View file

@ -39,7 +39,7 @@ pub struct AdminApiServer {
admin_token: Option<String>, admin_token: Option<String>,
} }
enum Endpoint { pub enum Endpoint {
Old(router_v1::Endpoint), Old(router_v1::Endpoint),
New(String), New(String),
} }
@ -159,7 +159,7 @@ impl ApiHandler for AdminApiServer {
AdminApiRequest::Options(req) => req.handle(&self.garage).await, AdminApiRequest::Options(req) => req.handle(&self.garage).await,
AdminApiRequest::CheckDomain(req) => req.handle(&self.garage).await, AdminApiRequest::CheckDomain(req) => req.handle(&self.garage).await,
AdminApiRequest::Health(req) => req.handle(&self.garage).await, AdminApiRequest::Health(req) => req.handle(&self.garage).await,
AdminApiRequest::Metrics(req) => self.handle_metrics(), AdminApiRequest::Metrics(_req) => self.handle_metrics(),
req => { req => {
let res = req.handle(&self.garage).await?; let res = req.handle(&self.garage).await?;
json_ok_response(&res) json_ok_response(&res)

View file

@ -457,7 +457,7 @@ impl EndpointHandler for GlobalAliasBucketRequest {
type Response = GlobalAliasBucketResponse; type Response = GlobalAliasBucketResponse;
async fn handle(self, garage: &Arc<Garage>) -> Result<GlobalAliasBucketResponse, Error> { async fn handle(self, garage: &Arc<Garage>) -> Result<GlobalAliasBucketResponse, Error> {
let bucket_id = parse_bucket_id(&self.id)?; let bucket_id = parse_bucket_id(&self.bucket_id)?;
let helper = garage.locked_helper().await; let helper = garage.locked_helper().await;
@ -476,7 +476,7 @@ impl EndpointHandler for GlobalUnaliasBucketRequest {
type Response = GlobalUnaliasBucketResponse; type Response = GlobalUnaliasBucketResponse;
async fn handle(self, garage: &Arc<Garage>) -> Result<GlobalUnaliasBucketResponse, Error> { async fn handle(self, garage: &Arc<Garage>) -> Result<GlobalUnaliasBucketResponse, Error> {
let bucket_id = parse_bucket_id(&self.id)?; let bucket_id = parse_bucket_id(&self.bucket_id)?;
let helper = garage.locked_helper().await; let helper = garage.locked_helper().await;
@ -495,7 +495,7 @@ impl EndpointHandler for LocalAliasBucketRequest {
type Response = LocalAliasBucketResponse; type Response = LocalAliasBucketResponse;
async fn handle(self, garage: &Arc<Garage>) -> Result<LocalAliasBucketResponse, Error> { async fn handle(self, garage: &Arc<Garage>) -> Result<LocalAliasBucketResponse, Error> {
let bucket_id = parse_bucket_id(&self.id)?; let bucket_id = parse_bucket_id(&self.bucket_id)?;
let helper = garage.locked_helper().await; let helper = garage.locked_helper().await;
@ -514,7 +514,7 @@ impl EndpointHandler for LocalUnaliasBucketRequest {
type Response = LocalUnaliasBucketResponse; type Response = LocalUnaliasBucketResponse;
async fn handle(self, garage: &Arc<Garage>) -> Result<LocalUnaliasBucketResponse, Error> { async fn handle(self, garage: &Arc<Garage>) -> Result<LocalUnaliasBucketResponse, Error> {
let bucket_id = parse_bucket_id(&self.id)?; let bucket_id = parse_bucket_id(&self.bucket_id)?;
let helper = garage.locked_helper().await; let helper = garage.locked_helper().await;

View file

@ -22,7 +22,7 @@ macro_rules! admin_endpoints {
} }
impl AdminApiRequest { impl AdminApiRequest {
fn name(&self) -> &'static str { pub fn name(&self) -> &'static str {
match self { match self {
$( $(
Self::$special_endpoint(_) => stringify!($special_endpoint), Self::$special_endpoint(_) => stringify!($special_endpoint),

View file

@ -43,22 +43,22 @@ impl AdminApiRequest {
POST UpdateKey (body_field, query::id), POST UpdateKey (body_field, query::id),
POST CreateKey (body), POST CreateKey (body),
POST ImportKey (body), POST ImportKey (body),
DELETE DeleteKey (query::id), POST DeleteKey (query::id),
GET ListKeys (), GET ListKeys (),
// Bucket endpoints // Bucket endpoints
GET GetBucketInfo (query_opt::id, query_opt::global_alias), GET GetBucketInfo (query_opt::id, query_opt::global_alias),
GET ListBuckets (), GET ListBuckets (),
POST CreateBucket (body), POST CreateBucket (body),
DELETE DeleteBucket (query::id), POST DeleteBucket (query::id),
PUT UpdateBucket (body_field, query::id), POST UpdateBucket (body_field, query::id),
// Bucket-key permissions // Bucket-key permissions
POST BucketAllowKey (body), POST BucketAllowKey (body),
POST BucketDenyKey (body), POST BucketDenyKey (body),
// Bucket aliases // Bucket aliases
PUT GlobalAliasBucket (query::id, query::alias), POST GlobalAliasBucket (body),
DELETE GlobalUnaliasBucket (query::id, query::alias), POST GlobalUnaliasBucket (body),
PUT LocalAliasBucket (query::id, query::access_key_id, query::alias), POST LocalAliasBucket (body),
DELETE LocalUnaliasBucket (query::id, query::access_key_id, query::alias), POST LocalUnaliasBucket (body),
]); ]);
if let Some(message) = query.nonempty_message() { if let Some(message) = query.nonempty_message() {
@ -131,7 +131,11 @@ impl AdminApiRequest {
let body = parse_json_body::<UpdateKeyRequestBody, _, Error>(req).await?; let body = parse_json_body::<UpdateKeyRequestBody, _, Error>(req).await?;
Ok(AdminApiRequest::UpdateKey(UpdateKeyRequest { id, body })) Ok(AdminApiRequest::UpdateKey(UpdateKeyRequest { id, body }))
} }
Endpoint::DeleteKey { id } => Ok(AdminApiRequest::DeleteKey(DeleteKeyRequest { id })),
// DeleteKey semantics changed:
// - in v1/ : HTTP DELETE => HTTP 204 No Content
// - in v2/ : HTTP POST => HTTP 200 Ok
// Endpoint::DeleteKey { id } => Ok(AdminApiRequest::DeleteKey(DeleteKeyRequest { id })),
// Buckets // Buckets
Endpoint::ListBuckets => Ok(AdminApiRequest::ListBuckets(ListBucketsRequest)), Endpoint::ListBuckets => Ok(AdminApiRequest::ListBuckets(ListBucketsRequest)),
@ -145,9 +149,13 @@ impl AdminApiRequest {
let req = parse_json_body::<CreateBucketRequest, _, Error>(req).await?; let req = parse_json_body::<CreateBucketRequest, _, Error>(req).await?;
Ok(AdminApiRequest::CreateBucket(req)) Ok(AdminApiRequest::CreateBucket(req))
} }
Endpoint::DeleteBucket { id } => {
Ok(AdminApiRequest::DeleteBucket(DeleteBucketRequest { id })) // DeleteBucket semantics changed::
} // - in v1/ : HTTP DELETE => HTTP 204 No Content
// - in v2/ : HTTP POST => HTTP 200 Ok
// Endpoint::DeleteBucket { id } => {
// Ok(AdminApiRequest::DeleteBucket(DeleteBucketRequest { id }))
// }
Endpoint::UpdateBucket { id } => { Endpoint::UpdateBucket { id } => {
let body = parse_json_body::<UpdateBucketRequestBody, _, Error>(req).await?; let body = parse_json_body::<UpdateBucketRequestBody, _, Error>(req).await?;
Ok(AdminApiRequest::UpdateBucket(UpdateBucketRequest { Ok(AdminApiRequest::UpdateBucket(UpdateBucketRequest {
@ -167,10 +175,16 @@ impl AdminApiRequest {
} }
// Bucket aliasing // Bucket aliasing
Endpoint::GlobalAliasBucket { id, alias } => Ok(AdminApiRequest::GlobalAliasBucket( Endpoint::GlobalAliasBucket { id, alias } => Ok(AdminApiRequest::GlobalAliasBucket(
GlobalAliasBucketRequest { id, alias }, GlobalAliasBucketRequest {
bucket_id: id,
alias,
},
)), )),
Endpoint::GlobalUnaliasBucket { id, alias } => Ok( Endpoint::GlobalUnaliasBucket { id, alias } => Ok(
AdminApiRequest::GlobalUnaliasBucket(GlobalUnaliasBucketRequest { id, alias }), AdminApiRequest::GlobalUnaliasBucket(GlobalUnaliasBucketRequest {
bucket_id: id,
alias,
}),
), ),
Endpoint::LocalAliasBucket { Endpoint::LocalAliasBucket {
id, id,
@ -178,7 +192,7 @@ impl AdminApiRequest {
alias, alias,
} => Ok(AdminApiRequest::LocalAliasBucket(LocalAliasBucketRequest { } => Ok(AdminApiRequest::LocalAliasBucket(LocalAliasBucketRequest {
access_key_id, access_key_id,
id, bucket_id: id,
alias, alias,
})), })),
Endpoint::LocalUnaliasBucket { Endpoint::LocalUnaliasBucket {
@ -188,7 +202,7 @@ impl AdminApiRequest {
} => Ok(AdminApiRequest::LocalUnaliasBucket( } => Ok(AdminApiRequest::LocalUnaliasBucket(
LocalUnaliasBucketRequest { LocalUnaliasBucketRequest {
access_key_id, access_key_id,
id, bucket_id: id,
alias, alias,
}, },
)), )),