From 3d20bfda009c308ad77bb9251abf493d047e080c Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 28 Jan 2025 16:18:48 +0100 Subject: [PATCH] admin api: update semantics of some endpoints, and update doc --- doc/drafts/admin-api.md | 110 +++++++++++++++++++++++++----------- src/api/admin/api.rs | 12 ++-- src/api/admin/api_server.rs | 4 +- src/api/admin/bucket.rs | 8 +-- src/api/admin/macros.rs | 2 +- src/api/admin/router_v2.rs | 44 ++++++++++----- 6 files changed, 122 insertions(+), 58 deletions(-) diff --git a/doc/drafts/admin-api.md b/doc/drafts/admin-api.md index a614af58..92b6a6db 100644 --- a/doc/drafts/admin-api.md +++ b/doc/drafts/admin-api.md @@ -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 when changes are introduced. -The Garage administration API was introduced in version 0.7.2, this document -does not apply to older versions of Garage. +The Garage administration API was introduced in version 0.7.2, and was +changed several times. +This document applies only to the Garage v2 API (starting with Garage v2.0.0). ## 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 for which a quorum of nodes is not available. 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=` + +Checks whether this Garage cluster serves a website for domain ``. +Returns HTTP 200 Ok if yes, or HTTP 4xx if no website is available for this domain. ### Cluster operations -#### GetClusterStatus `GET /v1/status` +#### GetClusterStatus `GET /v2/GetClusterStatus` Returns the cluster's current status in JSON, including: @@ -70,7 +78,7 @@ Example response body: ```json { "node": "b10c110e4e854e5aa3f4637681befac755154b20059ec163254ddbfae86b09df", - "garageVersion": "v1.0.1", + "garageVersion": "v2.0.0", "garageFeatures": [ "k2v", "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: @@ -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. @@ -232,7 +240,7 @@ Example response: ] ``` -#### GetClusterLayout `GET /v1/layout` +#### GetClusterLayout `GET /v2/GetClusterLayout` 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 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. -#### ApplyClusterLayout `POST /v1/layout/apply` +#### ApplyClusterLayout `POST /v2/ApplyClusterLayout` Applies to the cluster the layout changes currently registered as 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 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. @@ -374,7 +382,7 @@ as returned by GetClusterLayout. ### Access key operations -#### ListKeys `GET /v1/key` +#### ListKeys `GET /v2/ListKeys` Returns all API access keys in the cluster. @@ -393,8 +401,8 @@ Example response: ] ``` -#### GetKeyInfo `GET /v1/key?id=` -#### GetKeyInfo `GET /v1/key?search=` +#### GetKeyInfo `GET /v2/GetKeyInfo?id=` +#### GetKeyInfo `GET /v2/GetKeyInfo?search=` 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. @@ -483,7 +491,7 @@ Request body format: This returns the key info, including the created secret key, in the same format as the result of GetKeyInfo. -#### ImportKey `POST /v1/key/import` +#### ImportKey `POST /v2/ImportKey` Imports an existing API key. 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. -#### UpdateKey `POST /v1/key?id=` +#### UpdateKey `POST /v2/UpdateKey?id=` 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. -#### DeleteKey `DELETE /v1/key?id=` +#### DeleteKey `POST /v2/DeleteKey?id=` Deletes an API access key. ### Bucket operations -#### ListBuckets `GET /v1/bucket` +#### ListBuckets `GET /v2/ListBuckets` Returns all storage buckets in the cluster. @@ -572,8 +580,8 @@ Example response: ] ``` -#### GetBucketInfo `GET /v1/bucket?id=` -#### GetBucketInfo `GET /v1/bucket?globalAlias=` +#### GetBucketInfo `GET /v2/GetBucketInfo?id=` +#### GetBucketInfo `GET /v2/GetBucketInfo?globalAlias=` 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. @@ -656,7 +664,7 @@ or no alias at all. 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. -#### UpdateBucket `PUT /v1/bucket?id=` +#### UpdateBucket `POST /v2/UpdateBucket?id=` 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 change only one of the two quotas. -#### DeleteBucket `DELETE /v1/bucket?id=` +#### DeleteBucket `POST /v2/DeleteBucket?id=` 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 -#### BucketAllowKey `POST /v1/bucket/allow` +#### BucketAllowKey `POST /v2/BucketAllowKey` 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. 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. @@ -742,19 +750,57 @@ Other flags will remain unchanged. ### Operations on bucket aliases -#### GlobalAliasBucket `PUT /v1/bucket/alias/global?id=&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=&alias=` +Request body format: + +```json +{ + "bucketId": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b", + "alias": "the-bucket" +} +``` + +#### GlobalUnaliasBucket `POST /v2/GlobalUnaliasBucket` Removes a global alias for a bucket. -#### LocalAliasBucket `PUT /v1/bucket/alias/local?id=&accessKeyId=&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=&accessKeyId&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. +Request body format: + +```json +{ + "bucketId": "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b", + "accessKeyId": "GK31c2f218a2e44f485b94239e", + "alias": "my-bucket" +} +``` + diff --git a/src/api/admin/api.rs b/src/api/admin/api.rs index c8fad95b..457863e0 100644 --- a/src/api/admin/api.rs +++ b/src/api/admin/api.rs @@ -500,8 +500,9 @@ pub struct BucketDenyKeyResponse(pub GetBucketInfoResponse); // ---- GlobalAliasBucket ---- +#[derive(Deserialize)] pub struct GlobalAliasBucketRequest { - pub id: String, + pub bucket_id: String, pub alias: String, } @@ -510,8 +511,9 @@ pub struct GlobalAliasBucketResponse(pub GetBucketInfoResponse); // ---- GlobalUnaliasBucket ---- +#[derive(Deserialize)] pub struct GlobalUnaliasBucketRequest { - pub id: String, + pub bucket_id: String, pub alias: String, } @@ -520,8 +522,9 @@ pub struct GlobalUnaliasBucketResponse(pub GetBucketInfoResponse); // ---- LocalAliasBucket ---- +#[derive(Deserialize)] pub struct LocalAliasBucketRequest { - pub id: String, + pub bucket_id: String, pub access_key_id: String, pub alias: String, } @@ -531,8 +534,9 @@ pub struct LocalAliasBucketResponse(pub GetBucketInfoResponse); // ---- LocalUnaliasBucket ---- +#[derive(Deserialize)] pub struct LocalUnaliasBucketRequest { - pub id: String, + pub bucket_id: String, pub access_key_id: String, pub alias: String, } diff --git a/src/api/admin/api_server.rs b/src/api/admin/api_server.rs index 2f2e3284..82337b7e 100644 --- a/src/api/admin/api_server.rs +++ b/src/api/admin/api_server.rs @@ -39,7 +39,7 @@ pub struct AdminApiServer { admin_token: Option, } -enum Endpoint { +pub enum Endpoint { Old(router_v1::Endpoint), New(String), } @@ -159,7 +159,7 @@ impl ApiHandler for AdminApiServer { AdminApiRequest::Options(req) => req.handle(&self.garage).await, AdminApiRequest::CheckDomain(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 => { let res = req.handle(&self.garage).await?; json_ok_response(&res) diff --git a/src/api/admin/bucket.rs b/src/api/admin/bucket.rs index f9accba5..8e19b93e 100644 --- a/src/api/admin/bucket.rs +++ b/src/api/admin/bucket.rs @@ -457,7 +457,7 @@ impl EndpointHandler for GlobalAliasBucketRequest { type Response = GlobalAliasBucketResponse; async fn handle(self, garage: &Arc) -> Result { - let bucket_id = parse_bucket_id(&self.id)?; + let bucket_id = parse_bucket_id(&self.bucket_id)?; let helper = garage.locked_helper().await; @@ -476,7 +476,7 @@ impl EndpointHandler for GlobalUnaliasBucketRequest { type Response = GlobalUnaliasBucketResponse; async fn handle(self, garage: &Arc) -> Result { - let bucket_id = parse_bucket_id(&self.id)?; + let bucket_id = parse_bucket_id(&self.bucket_id)?; let helper = garage.locked_helper().await; @@ -495,7 +495,7 @@ impl EndpointHandler for LocalAliasBucketRequest { type Response = LocalAliasBucketResponse; async fn handle(self, garage: &Arc) -> Result { - let bucket_id = parse_bucket_id(&self.id)?; + let bucket_id = parse_bucket_id(&self.bucket_id)?; let helper = garage.locked_helper().await; @@ -514,7 +514,7 @@ impl EndpointHandler for LocalUnaliasBucketRequest { type Response = LocalUnaliasBucketResponse; async fn handle(self, garage: &Arc) -> Result { - let bucket_id = parse_bucket_id(&self.id)?; + let bucket_id = parse_bucket_id(&self.bucket_id)?; let helper = garage.locked_helper().await; diff --git a/src/api/admin/macros.rs b/src/api/admin/macros.rs index a12dc40b..d8c8f6dc 100644 --- a/src/api/admin/macros.rs +++ b/src/api/admin/macros.rs @@ -22,7 +22,7 @@ macro_rules! admin_endpoints { } impl AdminApiRequest { - fn name(&self) -> &'static str { + pub fn name(&self) -> &'static str { match self { $( Self::$special_endpoint(_) => stringify!($special_endpoint), diff --git a/src/api/admin/router_v2.rs b/src/api/admin/router_v2.rs index e0c54f0e..dacf6793 100644 --- a/src/api/admin/router_v2.rs +++ b/src/api/admin/router_v2.rs @@ -43,22 +43,22 @@ impl AdminApiRequest { POST UpdateKey (body_field, query::id), POST CreateKey (body), POST ImportKey (body), - DELETE DeleteKey (query::id), + POST DeleteKey (query::id), GET ListKeys (), // Bucket endpoints GET GetBucketInfo (query_opt::id, query_opt::global_alias), GET ListBuckets (), POST CreateBucket (body), - DELETE DeleteBucket (query::id), - PUT UpdateBucket (body_field, query::id), + POST DeleteBucket (query::id), + POST UpdateBucket (body_field, query::id), // Bucket-key permissions POST BucketAllowKey (body), POST BucketDenyKey (body), // Bucket aliases - PUT GlobalAliasBucket (query::id, query::alias), - DELETE GlobalUnaliasBucket (query::id, query::alias), - PUT LocalAliasBucket (query::id, query::access_key_id, query::alias), - DELETE LocalUnaliasBucket (query::id, query::access_key_id, query::alias), + POST GlobalAliasBucket (body), + POST GlobalUnaliasBucket (body), + POST LocalAliasBucket (body), + POST LocalUnaliasBucket (body), ]); if let Some(message) = query.nonempty_message() { @@ -131,7 +131,11 @@ impl AdminApiRequest { let body = parse_json_body::(req).await?; 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 Endpoint::ListBuckets => Ok(AdminApiRequest::ListBuckets(ListBucketsRequest)), @@ -145,9 +149,13 @@ impl AdminApiRequest { let req = parse_json_body::(req).await?; 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 } => { let body = parse_json_body::(req).await?; Ok(AdminApiRequest::UpdateBucket(UpdateBucketRequest { @@ -167,10 +175,16 @@ impl AdminApiRequest { } // Bucket aliasing Endpoint::GlobalAliasBucket { id, alias } => Ok(AdminApiRequest::GlobalAliasBucket( - GlobalAliasBucketRequest { id, alias }, + GlobalAliasBucketRequest { + bucket_id: id, + alias, + }, )), Endpoint::GlobalUnaliasBucket { id, alias } => Ok( - AdminApiRequest::GlobalUnaliasBucket(GlobalUnaliasBucketRequest { id, alias }), + AdminApiRequest::GlobalUnaliasBucket(GlobalUnaliasBucketRequest { + bucket_id: id, + alias, + }), ), Endpoint::LocalAliasBucket { id, @@ -178,7 +192,7 @@ impl AdminApiRequest { alias, } => Ok(AdminApiRequest::LocalAliasBucket(LocalAliasBucketRequest { access_key_id, - id, + bucket_id: id, alias, })), Endpoint::LocalUnaliasBucket { @@ -188,7 +202,7 @@ impl AdminApiRequest { } => Ok(AdminApiRequest::LocalUnaliasBucket( LocalUnaliasBucketRequest { access_key_id, - id, + bucket_id: id, alias, }, )),