admin api: generate openapi spec using utoipa (wip)

This commit is contained in:
Alex Auvolat 2025-03-06 11:51:48 +01:00
parent 2e03d90585
commit ba68506c36
12 changed files with 2335 additions and 1366 deletions

26
Cargo.lock generated
View file

@ -1288,6 +1288,7 @@ dependencies = [
"tokio",
"tracing",
"tracing-subscriber",
"utoipa",
]
[[package]]
@ -1318,6 +1319,7 @@ dependencies = [
"tokio",
"tracing",
"url",
"utoipa",
]
[[package]]
@ -2272,6 +2274,7 @@ checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
dependencies = [
"equivalent",
"hashbrown 0.15.2",
"serde",
]
[[package]]
@ -4768,6 +4771,29 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "utoipa"
version = "5.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435c6f69ef38c9017b4b4eea965dfb91e71e53d869e896db40d1cf2441dd75c0"
dependencies = [
"indexmap 2.7.1",
"serde",
"serde_json",
"utoipa-gen",
]
[[package]]
name = "utoipa-gen"
version = "5.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a77d306bc75294fd52f3e99b13ece67c02c1a2789190a6f31d32f736624326f7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "uuid"
version = "1.4.1"

View file

@ -101,6 +101,7 @@ serde = { version = "1.0", default-features = false, features = ["derive", "rc"]
serde_bytes = "0.11"
serde_json = "1.0"
toml = { version = "0.8", default-features = false, features = ["parse"] }
utoipa = "5.3.1"
# newer version requires rust edition 2021
k8s-openapi = { version = "0.21", features = ["v1_24"] }

View file

@ -18,7 +18,7 @@
</style>
</head>
<body>
<redoc spec-url='./garage-admin-v2.yml'></redoc>
<redoc spec-url='./garage-admin-v2.json'></redoc>
<script src="./redoc.standalone.js"> </script>
</body>
</html>

1734
doc/api/garage-admin-v2.json Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -38,6 +38,7 @@ url.workspace = true
serde.workspace = true
serde_json.workspace = true
utoipa.workspace = true
opentelemetry.workspace = true
opentelemetry-prometheus = { workspace = true, optional = true }

View file

@ -5,6 +5,7 @@ use std::sync::Arc;
use paste::paste;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use garage_rpc::*;
@ -155,18 +156,19 @@ pub struct MetricsRequest;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetClusterStatusRequest;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct GetClusterStatusResponse {
pub layout_version: u64,
pub nodes: Vec<NodeResp>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[derive(Debug, Clone, Serialize, Deserialize, Default, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct NodeResp {
pub id: String,
pub role: Option<NodeRoleResp>,
#[schema(value_type = Option<String> )]
pub addr: Option<SocketAddr>,
pub hostname: Option<String>,
pub is_up: bool,
@ -178,7 +180,7 @@ pub struct NodeResp {
pub metadata_partition: Option<FreeSpaceResp>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct NodeRoleResp {
pub id: String,
@ -187,7 +189,7 @@ pub struct NodeRoleResp {
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct FreeSpaceResp {
pub available: u64,
@ -199,28 +201,39 @@ pub struct FreeSpaceResp {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetClusterHealthRequest;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct GetClusterHealthResponse {
/// One of `healthy`, `degraded` or `unavailable`:
/// - healthy: Garage node is connected to all storage nodes
/// - degraded: Garage node is not connected to all storage nodes, but a quorum of write nodes is available for all partitions
/// - unavailable: a quorum of write nodes is not available for some partitions
pub status: String,
/// the number of nodes this Garage node has had a TCP connection to since the daemon started
pub known_nodes: usize,
/// the nubmer of nodes this Garage node currently has an open connection to
pub connected_nodes: usize,
/// the number of storage nodes currently registered in the cluster layout
pub storage_nodes: usize,
/// the number of storage nodes to which a connection is currently open
pub storage_nodes_ok: usize,
/// the total number of partitions of the data (currently always 256)
pub partitions: usize,
/// the number of partitions for which a quorum of write nodes is available
pub partitions_quorum: usize,
/// the number of partitions for which we are connected to all storage nodes responsible of storing it
pub partitions_all_ok: usize,
}
// ---- ConnectClusterNodes ----
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct ConnectClusterNodesRequest(pub Vec<String>);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct ConnectClusterNodesResponse(pub Vec<ConnectNodeResponse>);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ConnectNodeResponse {
pub success: bool,
@ -232,7 +245,7 @@ pub struct ConnectNodeResponse {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetClusterLayoutRequest;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct GetClusterLayoutResponse {
pub version: u64,
@ -240,7 +253,7 @@ pub struct GetClusterLayoutResponse {
pub staged_role_changes: Vec<NodeRoleChange>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct NodeRoleChange {
pub id: String,
@ -248,7 +261,7 @@ pub struct NodeRoleChange {
pub action: NodeRoleChangeEnum,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(untagged)]
pub enum NodeRoleChangeEnum {
#[serde(rename_all = "camelCase")]
@ -263,21 +276,21 @@ pub enum NodeRoleChangeEnum {
// ---- UpdateClusterLayout ----
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct UpdateClusterLayoutRequest(pub Vec<NodeRoleChange>);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct UpdateClusterLayoutResponse(pub GetClusterLayoutResponse);
// ---- ApplyClusterLayout ----
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ApplyClusterLayoutRequest {
pub version: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ApplyClusterLayoutResponse {
pub message: Vec<String>,
@ -289,7 +302,7 @@ pub struct ApplyClusterLayoutResponse {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RevertClusterLayoutRequest;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct RevertClusterLayoutResponse(pub GetClusterLayoutResponse);
// **********************************************
@ -301,10 +314,10 @@ pub struct RevertClusterLayoutResponse(pub GetClusterLayoutResponse);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListKeysRequest;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct ListKeysResponse(pub Vec<ListKeysResponseItem>);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ListKeysResponseItem {
pub id: String,
@ -320,7 +333,7 @@ pub struct GetKeyInfoRequest {
pub show_secret_key: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct GetKeyInfoResponse {
pub name: String,
@ -331,14 +344,14 @@ pub struct GetKeyInfoResponse {
pub buckets: Vec<KeyInfoBucketResponse>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct KeyPerm {
#[serde(default)]
pub create_bucket: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct KeyInfoBucketResponse {
pub id: String,
@ -347,7 +360,7 @@ pub struct KeyInfoBucketResponse {
pub permissions: ApiBucketKeyPerm,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[derive(Debug, Clone, Serialize, Deserialize, Default, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ApiBucketKeyPerm {
#[serde(default)]
@ -360,18 +373,18 @@ pub struct ApiBucketKeyPerm {
// ---- CreateKey ----
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct CreateKeyRequest {
pub name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct CreateKeyResponse(pub GetKeyInfoResponse);
// ---- ImportKey ----
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ImportKeyRequest {
pub access_key_id: String,
@ -379,7 +392,7 @@ pub struct ImportKeyRequest {
pub name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct ImportKeyResponse(pub GetKeyInfoResponse);
// ---- UpdateKey ----
@ -390,10 +403,10 @@ pub struct UpdateKeyRequest {
pub body: UpdateKeyRequestBody,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct UpdateKeyResponse(pub GetKeyInfoResponse);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct UpdateKeyRequestBody {
pub name: Option<String>,
@ -420,10 +433,10 @@ pub struct DeleteKeyResponse;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListBucketsRequest;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct ListBucketsResponse(pub Vec<ListBucketsResponseItem>);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ListBucketsResponseItem {
pub id: String,
@ -431,7 +444,7 @@ pub struct ListBucketsResponseItem {
pub local_aliases: Vec<BucketLocalAlias>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct BucketLocalAlias {
pub access_key_id: String,
@ -447,32 +460,44 @@ pub struct GetBucketInfoRequest {
pub search: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct GetBucketInfoResponse {
/// Identifier of the bucket
pub id: String,
/// List of global aliases for this bucket
pub global_aliases: Vec<String>,
/// Whether website acces is enabled for this bucket
pub website_access: bool,
#[serde(default)]
/// Website configuration for this bucket
pub website_config: Option<GetBucketInfoWebsiteResponse>,
/// List of access keys that have permissions granted on this bucket
pub keys: Vec<GetBucketInfoKey>,
/// Number of objects in this bucket
pub objects: i64,
/// Total number of bytes used by objects in this bucket
pub bytes: i64,
/// Number of unfinished uploads in this bucket
pub unfinished_uploads: i64,
/// Number of unfinished multipart uploads in this bucket
pub unfinished_multipart_uploads: i64,
/// Number of parts in unfinished multipart uploads in this bucket
pub unfinished_multipart_upload_parts: i64,
/// Total number of bytes used by unfinished multipart uploads in this bucket
pub unfinished_multipart_upload_bytes: i64,
/// Quotas that apply to this bucket
pub quotas: ApiBucketQuotas,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct GetBucketInfoWebsiteResponse {
pub index_document: String,
pub error_document: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct GetBucketInfoKey {
pub access_key_id: String,
@ -481,7 +506,7 @@ pub struct GetBucketInfoKey {
pub bucket_local_aliases: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ApiBucketQuotas {
pub max_size: Option<u64>,
@ -490,17 +515,17 @@ pub struct ApiBucketQuotas {
// ---- CreateBucket ----
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct CreateBucketRequest {
pub global_alias: Option<String>,
pub local_alias: Option<CreateBucketLocalAlias>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct CreateBucketResponse(pub GetBucketInfoResponse);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct CreateBucketLocalAlias {
pub access_key_id: String,
@ -517,17 +542,17 @@ pub struct UpdateBucketRequest {
pub body: UpdateBucketRequestBody,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct UpdateBucketResponse(pub GetBucketInfoResponse);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct UpdateBucketRequestBody {
pub website_access: Option<UpdateBucketWebsiteAccess>,
pub quotas: Option<ApiBucketQuotas>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct UpdateBucketWebsiteAccess {
pub enabled: bool,
@ -547,13 +572,13 @@ pub struct DeleteBucketResponse;
// ---- CleanupIncompleteUploads ----
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct CleanupIncompleteUploadsRequest {
pub bucket_id: String,
pub older_than_secs: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct CleanupIncompleteUploadsResponse {
pub uploads_deleted: u64,
}
@ -564,13 +589,13 @@ pub struct CleanupIncompleteUploadsResponse {
// ---- AllowBucketKey ----
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct AllowBucketKeyRequest(pub BucketKeyPermChangeRequest);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct AllowBucketKeyResponse(pub GetBucketInfoResponse);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct BucketKeyPermChangeRequest {
pub bucket_id: String,
@ -580,10 +605,10 @@ pub struct BucketKeyPermChangeRequest {
// ---- DenyBucketKey ----
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct DenyBucketKeyRequest(pub BucketKeyPermChangeRequest);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct DenyBucketKeyResponse(pub GetBucketInfoResponse);
// **********************************************
@ -592,7 +617,7 @@ pub struct DenyBucketKeyResponse(pub GetBucketInfoResponse);
// ---- AddBucketAlias ----
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct AddBucketAliasRequest {
pub bucket_id: String,
@ -600,10 +625,10 @@ pub struct AddBucketAliasRequest {
pub alias: BucketAliasEnum,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct AddBucketAliasResponse(pub GetBucketInfoResponse);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(untagged)]
pub enum BucketAliasEnum {
#[serde(rename_all = "camelCase")]
@ -617,7 +642,7 @@ pub enum BucketAliasEnum {
// ---- RemoveBucketAlias ----
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct RemoveBucketAliasRequest {
pub bucket_id: String,
@ -625,7 +650,7 @@ pub struct RemoveBucketAliasRequest {
pub alias: BucketAliasEnum,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct RemoveBucketAliasResponse(pub GetBucketInfoResponse);
// **********************************************

View file

@ -6,6 +6,7 @@ mod error;
mod macros;
pub mod api;
pub mod openapi;
mod router_v0;
mod router_v1;
mod router_v2;

483
src/api/admin/openapi.rs Normal file
View file

@ -0,0 +1,483 @@
#![allow(dead_code)]
#![allow(non_snake_case)]
use utoipa::{OpenApi, Modify};
use crate::api::*;
// **********************************************
// Cluster operations
// **********************************************
#[utoipa::path(get,
path = "/v2/GetClusterStatus",
tag = "Nodes",
description = "
Returns the cluster's current status, including:
- ID of the node being queried and its version of the Garage daemon
- Live nodes
- Currently configured cluster layout
- Staged changes to the cluster layout
*Capacity is given in bytes*
",
responses(
(status = 200, description = "Cluster status report", body = GetClusterStatusResponse),
(status = 500, description = "Internal server error")
),
)]
fn GetClusterStatus() -> () {}
#[utoipa::path(get,
path = "/v2/GetClusterHealth",
tag = "Nodes",
description = "Returns the global status of the cluster, the number of connected nodes (over the number of known ones), the number of healthy storage nodes (over the declared ones), and the number of healthy partitions (over the total).",
responses(
(status = 200, description = "Cluster health report", body = GetClusterHealthResponse),
),
)]
fn GetClusterHealth() -> () {}
#[utoipa::path(post,
path = "/v2/ConnectClusterNodes",
tag = "Nodes",
description = "Instructs this Garage node to connect to other Garage nodes at specified `<node_id>@<net_address>`. `node_id` is generated automatically on node start.",
request_body=ConnectClusterNodesRequest,
responses(
(status = 200, description = "The request has been handled correctly but it does not mean that all connection requests succeeded; some might have fail, you need to check the body!", body = ConnectClusterNodesResponse),
(status = 500, description = "Internal server error")
),
)]
fn ConnectClusterNodes() -> () {}
#[utoipa::path(get,
path = "/v2/GetClusterLayout",
tag = "Layout",
description = "
Returns the cluster's current layout, including:
- Currently configured cluster layout
- Staged changes to the cluster layout
*Capacity is given in bytes*
*The info returned by this endpoint is a subset of the info returned by `GET /GetClusterStatus`.*
",
responses(
(status = 200, description = "Current cluster layout", body = GetClusterLayoutResponse),
(status = 500, description = "Internal server error")
),
)]
fn GetClusterLayout() -> () {}
#[utoipa::path(post,
path = "/v2/UpdateClusterLayout",
tag = "Layout",
description = "
Send modifications to the cluster layout. These modifications will be included in the staged role changes, visible in subsequent calls of `GET /GetClusterHealth`. Once the set of staged changes is satisfactory, the user may call `POST /ApplyClusterLayout` to apply the changed changes, or `POST /RevertClusterLayout` to clear all of the staged changes in the layout.
Setting the capacity to `null` will configure the node as a gateway.
Otherwise, capacity must be now set in bytes (before Garage 0.9 it was arbitrary weights).
For example to declare 100GB, you must set `capacity: 100000000000`.
Garage uses internally the International System of Units (SI), it assumes that 1kB = 1000 bytes, and displays storage as kB, MB, GB (and not KiB, MiB, GiB that assume 1KiB = 1024 bytes).
",
request_body(
content=UpdateClusterLayoutRequest,
description="
To add a new node to the layout or to change the configuration of an existing node, simply set the values you want (`zone`, `capacity`, and `tags`).
To remove a node, simply pass the `remove: true` field.
This logic is represented in OpenAPI with a 'One Of' object.
Contrary to the CLI that may update only a subset of the fields capacity, zone and tags, when calling this API all of these values must be specified.
"
),
responses(
(status = 200, description = "Proposed changes have been added to the list of pending changes", body = UpdateClusterLayoutResponse),
(status = 500, description = "Internal server error")
),
)]
fn UpdateClusterLayout() -> () {}
#[utoipa::path(post,
path = "/v2/ApplyClusterLayout",
tag = "Layout",
description = "
Applies to the cluster the layout changes currently registered as staged layout changes.
*Note: do not try to parse the `message` field of the response, it is given as an array of string specifically because its format is not stable.*
",
request_body=ApplyClusterLayoutRequest,
responses(
(status = 200, description = "The updated cluster layout has been applied in the cluster", body = ApplyClusterLayoutResponse),
(status = 500, description = "Internal server error")
),
)]
fn ApplyClusterLayout() -> () {}
#[utoipa::path(post,
path = "/v2/RevertClusterLayout",
tag = "Layout",
description = "Clear staged layout",
responses(
(status = 200, description = "All pending changes to the cluster layout have been erased", body = RevertClusterLayoutResponse),
(status = 500, description = "Internal server error")
),
)]
fn RevertClusterLayout() -> () {}
// **********************************************
// Access key operations
// **********************************************
#[utoipa::path(get,
path = "/v2/ListKeys",
tag = "Key",
description = "Returns all API access keys in the cluster.",
responses(
(status = 200, description = "Returns the key identifier (aka `AWS_ACCESS_KEY_ID`) and its associated, human friendly, name if any (otherwise return an empty string)", body = ListKeysResponse),
(status = 500, description = "Internal server error")
),
)]
fn ListKeys() -> () {}
#[utoipa::path(get,
path = "/v2/GetKeyInfo",
tag = "Key",
description = "
Return information about a specific key like its identifiers, its permissions and buckets on which it has permissions.
You can search by specifying the exact key identifier (`id`) or by specifying a pattern (`search`).
For confidentiality reasons, the secret key is not returned by default: you must pass the `showSecretKey` query parameter to get it.
",
params(
("id", description = "Access key ID"),
("search", description = "Partial key ID or name to search for"),
("showSecretKey", description = "Whether to return the secret access key"),
),
responses(
(status = 200, description = "Information about the access key", body = GetKeyInfoResponse),
(status = 500, description = "Internal server error")
),
)]
fn GetKeyInfo() -> () {}
#[utoipa::path(post,
path = "/v2/CreateKey",
tag = "Key",
description = "Creates a new API access key.",
request_body = CreateKeyRequest,
responses(
(status = 200, description = "Access key has been created", body = CreateKeyResponse),
(status = 500, description = "Internal server error")
),
)]
fn CreateKey() -> () {}
#[utoipa::path(post,
path = "/v2/ImportKey",
tag = "Key",
description = "
Imports an existing API key. This feature must only be used for migrations and backup restore.
**Do not use it to generate custom key identifiers or you will break your Garage cluster.**
",
request_body = ImportKeyRequest,
responses(
(status = 200, description = "Access key has been imported", body = ImportKeyResponse),
(status = 500, description = "Internal server error")
),
)]
fn ImportKey() -> () {}
#[utoipa::path(post,
path = "/v2/UpdateKey",
tag = "Key",
description = "
Updates information about the specified API access key.
*Note: the secret key is not returned in the response, `null` is sent instead.*
",
request_body = UpdateKeyRequestBody,
params(
("id", description = "Access key ID"),
),
responses(
(status = 200, description = "Access key has been updated", body = UpdateKeyResponse),
(status = 500, description = "Internal server error")
),
)]
fn UpdateKey() -> () {}
#[utoipa::path(post,
path = "/v2/DeleteKey",
tag = "Key",
description = "Delete a key from the cluster. Its access will be removed from all the buckets. Buckets are not automatically deleted and can be dangling. You should manually delete them before. ",
params(
("id", description = "Access key ID"),
),
responses(
(status = 200, description = "Access key has been deleted"),
(status = 500, description = "Internal server error")
),
)]
fn DeleteKey() -> () {}
// **********************************************
// Bucket operations
// **********************************************
#[utoipa::path(get,
path = "/v2/ListBuckets",
tag = "Bucket",
description = "List all the buckets on the cluster with their UUID and their global and local aliases.",
responses(
(status = 200, description = "Returns the UUID of all the buckets and all their aliases", body = ListBucketsResponse),
(status = 500, description = "Internal server error")
),
)]
fn ListBuckets() -> () {}
#[utoipa::path(get,
path = "/v2/GetBucketInfo",
tag = "Bucket",
description = "
Given a bucket identifier (`id`) or a global alias (`alias`), get its information.
It includes its aliases, its web configuration, keys that have some permissions
on it, some statistics (number of objects, size), number of dangling multipart uploads,
and its quotas (if any).
",
params(
("id", description = "Exact bucket ID to look up"),
("globalAlias", description = "Global alias of bucket to look up"),
("search", description = "Partial ID or alias to search for"),
),
responses(
(status = 200, description = "Returns exhaustive information about the bucket", body = GetBucketInfoResponse),
(status = 500, description = "Internal server error")
),
)]
fn GetBucketInfo() -> () {}
#[utoipa::path(post,
path = "/v2/CreateBucket",
tag = "Bucket",
description = "
Creates a new bucket, either with a global alias, a local one, or no alias at all.
Technically, you can also specify both `globalAlias` and `localAlias` and that would create two aliases.
",
request_body = CreateBucketRequest,
responses(
(status = 200, description = "Returns exhaustive information about the bucket", body = CreateBucketResponse),
(status = 500, description = "Internal server error")
),
)]
fn CreateBucket() -> () {}
#[utoipa::path(post,
path = "/v2/UpdateBucket",
tag = "Bucket",
description = "
All fields (`websiteAccess` and `quotas`) are optional.
If they are present, the corresponding modifications are applied to the bucket, otherwise nothing is changed.
In `websiteAccess`: if `enabled` is `true`, `indexDocument` must be specified.
The field `errorDocument` is optional, if no error document is set a generic
error message is displayed when errors happen. Conversely, if `enabled` is
`false`, neither `indexDocument` nor `errorDocument` must be specified.
In `quotas`: new values of `maxSize` and `maxObjects` must both be specified, or set to `null`
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.
",
params(
("id", description = "ID of the bucket to update"),
),
request_body = UpdateBucketRequestBody,
responses(
(status = 200, description = "Bucket has been updated", body = UpdateBucketResponse),
(status = 404, description = "Bucket not found"),
(status = 500, description = "Internal server error")
),
)]
fn UpdateBucket() -> () {}
#[utoipa::path(post,
path = "/v2/DeleteBucket",
tag = "Bucket",
description = "
Deletes a storage bucket. A bucket cannot be deleted if it is not empty.
**Warning:** this will delete all aliases associated with the bucket!
",
params(
("id", description = "ID of the bucket to delete"),
),
responses(
(status = 200, description = "Bucket has been deleted"),
(status = 400, description = "Bucket is not empty"),
(status = 404, description = "Bucket not found"),
(status = 500, description = "Internal server error")
),
)]
fn DeleteBucket() -> () {}
#[utoipa::path(post,
path = "/v2/CleanupIncompleteUploads",
tag = "Bucket",
description = "Removes all incomplete multipart uploads that are older than the specified number of seconds.",
request_body = CleanupIncompleteUploadsRequest,
responses(
(status = 200, description = "The bucket was cleaned up successfully", body = CleanupIncompleteUploadsResponse),
(status = 500, description = "Internal server error")
),
)]
fn CleanupIncompleteUploads() -> () {}
// **********************************************
// Operations on permissions for keys on buckets
// **********************************************
#[utoipa::path(post,
path = "/v2/AllowBucketKey",
tag = "Permission",
description = "
**DISCLAIMER**: Garage's developers are aware that this endpoint has an unconventional semantic. Be extra careful when implementing it, its behavior is not obvious.
Allows a key to do read/write/owner operations on a bucket.
Flags in permissions which have the value true will be activated. Other flags will remain unchanged (ie. they will keep their internal value).
For example, if you set read to true, the key will be allowed to read the bucket.
If you set it to false, the key will keeps its previous read permission.
If you want to disallow read for the key, check the DenyBucketKey operation.
",
request_body = AllowBucketKeyRequest,
responses(
(status = 200, description = "Returns exhaustive information about the bucket", body = AllowBucketKeyResponse),
(status = 500, description = "Internal server error")
),
)]
fn AllowBucketKey() -> () {}
#[utoipa::path(post,
path = "/v2/DenyBucketKey",
tag = "Permission",
description = "
**DISCLAIMER**: Garage's developers are aware that this endpoint has an unconventional semantic. Be extra careful when implementing it, its behavior is not obvious.
Denies a key from doing read/write/owner operations on a bucket.
Flags in permissions which have the value true will be deactivated. Other flags will remain unchanged.
For example, if you set read to true, the key will be denied from reading.
If you set read to false, the key will keep its previous permissions.
If you want the key to have the reading permission, check the AllowBucketKey operation.
",
request_body = DenyBucketKeyRequest,
responses(
(status = 200, description = "Returns exhaustive information about the bucket", body = DenyBucketKeyResponse),
(status = 500, description = "Internal server error")
),
)]
fn DenyBucketKey() -> () {}
// **********************************************
// Operations on bucket aliases
// **********************************************
#[utoipa::path(post,
path = "/v2/AddBucketAlias",
tag = "Alias",
description = "Add an alias for the target bucket. This can be a local alias if `accessKeyId` is specified, or a global alias otherwise.",
request_body = AddBucketAliasRequest,
responses(
(status = 200, description = "Returns exhaustive information about the bucket", body = AddBucketAliasResponse),
(status = 500, description = "Internal server error")
),
)]
fn AddBucketAlias() -> () {}
#[utoipa::path(post,
path = "/v2/RemoveBucketAlias",
tag = "Alias",
description = "Remove an alias for the target bucket. This can be a local alias if `accessKeyId` is specified, or a global alias otherwise.",
request_body = RemoveBucketAliasRequest,
responses(
(status = 200, description = "Returns exhaustive information about the bucket", body = RemoveBucketAliasResponse),
(status = 500, description = "Internal server error")
),
)]
fn RemoveBucketAlias() -> () {}
// **********************************************
// **********************************************
// **********************************************
struct SecurityAddon;
impl Modify for SecurityAddon {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
use utoipa::openapi::security::*;
let components = openapi.components.as_mut().unwrap(); // we can unwrap safely since there already is components registered.
components.add_security_scheme(
"bearerAuth",
SecurityScheme::Http(Http::builder()
.scheme(HttpAuthScheme::Bearer)
.build()),
)
}
}
#[derive(OpenApi)]
#[openapi(
info(
version = "v2.0.0",
title = "Garage administration API",
description = "Administrate your Garage cluster programatically, including status, layout, keys, buckets, and maintainance tasks.
*Disclaimer: This API may change in future Garage versions. Read the changelog and upgrade your scripts before upgrading. Additionnaly, this specification is very early stage and can contain bugs, especially on error return codes/types that are not tested yet. Do not expect a well finished and polished product!*",
contact(
name = "The Garage team",
email = "garagehq@deuxfleurs.fr",
url = "https://garagehq.deuxfleurs.fr/",
),
),
modifiers(&SecurityAddon),
security(("bearerAuth" = [])),
paths(
// Cluster operations
GetClusterHealth,
GetClusterStatus,
ConnectClusterNodes,
GetClusterLayout,
UpdateClusterLayout,
ApplyClusterLayout,
RevertClusterLayout,
// Key operations
ListKeys,
GetKeyInfo,
CreateKey,
ImportKey,
UpdateKey,
DeleteKey,
// Bucket operations
ListBuckets,
GetBucketInfo,
CreateBucket,
UpdateBucket,
DeleteBucket,
CleanupIncompleteUploads,
// Operations on permissions
AllowBucketKey,
DenyBucketKey,
// Operations on aliases
AddBucketAlias,
RemoveBucketAlias,
),
servers(
(url = "http://localhost:3903/", description = "A local server")
),
)]
pub struct ApiDoc;

View file

@ -48,6 +48,7 @@ sha1.workspace = true
sodiumoxide.workspace = true
structopt.workspace = true
git-version.workspace = true
utoipa.workspace = true
futures.workspace = true
tokio.workspace = true

View file

@ -58,6 +58,10 @@ pub enum Command {
/// Convert metadata db between database engine formats
#[structopt(name = "convert-db", version = garage_version())]
ConvertDb(convert_db::ConvertDbOpt),
/// Output openapi JSON schema for admin api
#[structopt(name = "admin-api-schema", version = garage_version(), setting(structopt::clap::AppSettings::Hidden))]
AdminApiSchema,
}
#[derive(StructOpt, Debug)]

View file

@ -24,6 +24,7 @@ use std::net::SocketAddr;
use std::path::PathBuf;
use structopt::StructOpt;
use utoipa::OpenApi;
use garage_net::util::parse_and_resolve_peer_addr;
use garage_net::NetworkKey;
@ -151,6 +152,10 @@ async fn main() {
Command::Node(NodeOperation::NodeId(node_id_opt)) => {
cli::init::node_id_command(opt.config_file, node_id_opt.quiet)
}
Command::AdminApiSchema => {
println!("{}", garage_api_admin::openapi::ApiDoc::openapi().to_pretty_json().unwrap());
Ok(())
}
_ => cli_command(opt).await,
};