openapi: work around issue for flattenned untagged enum (fix #1249)

This commit is contained in:
Alex Auvolat
2026-01-06 09:55:42 +01:00
parent 02677af546
commit cf2f058f60
2 changed files with 113 additions and 43 deletions
+51 -39
View File
@@ -103,7 +103,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/AddBucketAliasRequest" "$ref": "#/components/schemas/BucketAliasEnum"
} }
} }
}, },
@@ -1409,7 +1409,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/RemoveBucketAliasRequest" "$ref": "#/components/schemas/BucketAliasEnum"
} }
} }
}, },
@@ -1722,24 +1722,6 @@
}, },
"components": { "components": {
"schemas": { "schemas": {
"AddBucketAliasRequest": {
"allOf": [
{
"$ref": "#/components/schemas/BucketAliasEnum"
},
{
"type": "object",
"required": [
"bucketId"
],
"properties": {
"bucketId": {
"type": "string"
}
}
}
]
},
"AddBucketAliasResponse": { "AddBucketAliasResponse": {
"$ref": "#/components/schemas/GetBucketInfoResponse" "$ref": "#/components/schemas/GetBucketInfoResponse"
}, },
@@ -1957,9 +1939,13 @@
{ {
"type": "object", "type": "object",
"required": [ "required": [
"bucketId",
"globalAlias" "globalAlias"
], ],
"properties": { "properties": {
"bucketId": {
"type": "string"
},
"globalAlias": { "globalAlias": {
"type": "string" "type": "string"
} }
@@ -1968,6 +1954,7 @@
{ {
"type": "object", "type": "object",
"required": [ "required": [
"bucketId",
"localAlias", "localAlias",
"accessKeyId" "accessKeyId"
], ],
@@ -1975,6 +1962,9 @@
"accessKeyId": { "accessKeyId": {
"type": "string" "type": "string"
}, },
"bucketId": {
"type": "string"
},
"localAlias": { "localAlias": {
"type": "string" "type": "string"
} }
@@ -3912,6 +3902,46 @@
} }
] ]
}, },
"NodeRoleChangeRequest": {
"oneOf": [
{
"type": "object",
"required": [
"id",
"remove"
],
"properties": {
"id": {
"type": "string",
"description": "ID of the node for which this change applies"
},
"remove": {
"type": "boolean",
"description": "Set `remove` to `true` to remove the node from the layout"
}
}
},
{
"allOf": [
{
"$ref": "#/components/schemas/NodeAssignedRole"
},
{
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "ID of the node for which this change applies"
}
}
}
]
}
]
},
"NodeUpdateTrackers": { "NodeUpdateTrackers": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -3973,24 +4003,6 @@
} }
] ]
}, },
"RemoveBucketAliasRequest": {
"allOf": [
{
"$ref": "#/components/schemas/BucketAliasEnum"
},
{
"type": "object",
"required": [
"bucketId"
],
"properties": {
"bucketId": {
"type": "string"
}
}
}
]
},
"RemoveBucketAliasResponse": { "RemoveBucketAliasResponse": {
"$ref": "#/components/schemas/GetBucketInfoResponse" "$ref": "#/components/schemas/GetBucketInfoResponse"
}, },
@@ -4180,7 +4192,7 @@
"roles": { "roles": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/components/schemas/NodeRoleChange" "$ref": "#/components/schemas/NodeRoleChangeRequest"
}, },
"description": "New node roles to assign or remove in the cluster layout" "description": "New node roles to assign or remove in the cluster layout"
} }
+62 -4
View File
@@ -1,7 +1,8 @@
#![allow(dead_code)] #![allow(dead_code)]
#![allow(non_snake_case)] #![allow(non_snake_case)]
use utoipa::{Modify, OpenApi}; use serde::{Deserialize, Serialize};
use utoipa::{Modify, OpenApi, ToSchema};
use crate::api::*; use crate::api::*;
@@ -246,7 +247,7 @@ 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). 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( request_body(
content=UpdateClusterLayoutRequest, content=UpdateClusterLayoutRequestOpenapi,
description=" 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 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. To remove a node, simply pass the `remove: true` field.
@@ -262,6 +263,45 @@ Contrary to the CLI that may update only a subset of the fields capacity, zone a
)] )]
fn UpdateClusterLayout() -> () {} fn UpdateClusterLayout() -> () {}
// Hack: we cannot use the UpdateClusterLayoutRequest from api.rs,
// as it contains (via NodeRoleChange) an untagged enum flattenned into
// a struct, which breaks the openapi generator.
// See issue #1249.
// Instead, we use a rewritten version of the NodeRoleChange struct where
// the struct fields are distributed into the enum variants (this is an equivalent
// representation, but this way we avoid having to rewrite all uses of the original
// struct in the Garage codebase).
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[schema(as = UpdateClusterLayoutRequest)]
pub struct UpdateClusterLayoutRequestOpenapi {
/// New node roles to assign or remove in the cluster layout
#[serde(default)]
pub roles: Vec<NodeRoleChangeOpenapi>,
/// New layout computation parameters to use
#[serde(default)]
pub parameters: Option<LayoutParameters>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[schema(as = NodeRoleChangeRequest)]
#[serde(untagged)]
pub enum NodeRoleChangeOpenapi {
#[serde(rename_all = "camelCase")]
Remove {
/// ID of the node for which this change applies
id: String,
/// Set `remove` to `true` to remove the node from the layout
remove: bool,
},
#[serde(rename_all = "camelCase")]
Update {
/// ID of the node for which this change applies
id: String,
#[serde(flatten)]
role: NodeAssignedRole,
},
}
#[utoipa::path(post, #[utoipa::path(post,
path = "/v2/PreviewClusterLayoutChanges", path = "/v2/PreviewClusterLayoutChanges",
tag = "Cluster layout", tag = "Cluster layout",
@@ -586,7 +626,7 @@ fn DenyBucketKey() -> () {}
path = "/v2/AddBucketAlias", path = "/v2/AddBucketAlias",
tag = "Bucket alias", tag = "Bucket alias",
description = "Add an alias for the target bucket. This can be either a global or a local alias, depending on which fields are specified.", description = "Add an alias for the target bucket. This can be either a global or a local alias, depending on which fields are specified.",
request_body = AddBucketAliasRequest, request_body = BucketAliasEnumOpenapi,
responses( responses(
(status = 200, description = "Returns exhaustive information about the bucket", body = AddBucketAliasResponse), (status = 200, description = "Returns exhaustive information about the bucket", body = AddBucketAliasResponse),
(status = 500, description = "Internal server error") (status = 500, description = "Internal server error")
@@ -598,7 +638,7 @@ fn AddBucketAlias() -> () {}
path = "/v2/RemoveBucketAlias", path = "/v2/RemoveBucketAlias",
tag = "Bucket alias", tag = "Bucket alias",
description = "Remove an alias for the target bucket. This can be either a global or a local alias, depending on which fields are specified.", description = "Remove an alias for the target bucket. This can be either a global or a local alias, depending on which fields are specified.",
request_body = RemoveBucketAliasRequest, request_body = BucketAliasEnumOpenapi,
responses( responses(
(status = 200, description = "Returns exhaustive information about the bucket", body = RemoveBucketAliasResponse), (status = 200, description = "Returns exhaustive information about the bucket", body = RemoveBucketAliasResponse),
(status = 500, description = "Internal server error") (status = 500, description = "Internal server error")
@@ -606,6 +646,24 @@ fn AddBucketAlias() -> () {}
)] )]
fn RemoveBucketAlias() -> () {} fn RemoveBucketAlias() -> () {}
// Hack for issue #1249 (see UpdateClusterLayout)
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(untagged)]
#[schema(as = BucketAliasEnum)]
pub enum BucketAliasEnumOpenapi {
#[serde(rename_all = "camelCase")]
Global {
bucket_id: String,
global_alias: String,
},
#[serde(rename_all = "camelCase")]
Local {
bucket_id: String,
local_alias: String,
access_key_id: String,
},
}
// ********************************************** // **********************************************
// Node operations // Node operations
// ********************************************** // **********************************************