From e4881e62f116ffc22717f3c46dff84d827f20811 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Thu, 6 Mar 2025 17:12:52 +0100 Subject: [PATCH] admin api: management of layout parameters through admin api --- doc/api/garage-admin-v2.json | 68 +++++++++++++++++++++++++++++-- src/api/admin/api.rs | 28 +++++++++++-- src/api/admin/cluster.rs | 78 +++++++++++++++++++++++++++++++++--- src/api/admin/router_v2.rs | 5 +-- src/garage/cli/layout.rs | 48 ---------------------- src/garage/cli_v2/layout.rs | 50 +++++++++++++++++++---- 6 files changed, 204 insertions(+), 73 deletions(-) diff --git a/doc/api/garage-admin-v2.json b/doc/api/garage-admin-v2.json index e7b42620..a13252b3 100644 --- a/doc/api/garage-admin-v2.json +++ b/doc/api/garage-admin-v2.json @@ -1875,15 +1875,29 @@ "required": [ "version", "roles", + "parameters", "stagedRoleChanges" ], "properties": { + "parameters": { + "$ref": "#/components/schemas/LayoutParameters" + }, "roles": { "type": "array", "items": { "$ref": "#/components/schemas/NodeRoleResp" } }, + "stagedParameters": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/LayoutParameters" + } + ] + }, "stagedRoleChanges": { "type": "array", "items": { @@ -2021,6 +2035,17 @@ } } }, + "LayoutParameters": { + "type": "object", + "required": [ + "zoneRedundancy" + ], + "properties": { + "zoneRedundancy": { + "$ref": "#/components/schemas/ZoneRedundancy" + } + } + }, "ListBucketsResponse": { "type": "array", "items": { @@ -3109,9 +3134,24 @@ } }, "UpdateClusterLayoutRequest": { - "type": "array", - "items": { - "$ref": "#/components/schemas/NodeRoleChange" + "type": "object", + "properties": { + "parameters": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/LayoutParameters" + } + ] + }, + "roles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NodeRoleChange" + } + } } }, "UpdateClusterLayoutResponse": { @@ -3289,6 +3329,28 @@ ] } ] + }, + "ZoneRedundancy": { + "oneOf": [ + { + "type": "object", + "required": [ + "atLeast" + ], + "properties": { + "atLeast": { + "type": "integer", + "minimum": 0 + } + } + }, + { + "type": "string", + "enum": [ + "maximum" + ] + } + ] } }, "securitySchemes": { diff --git a/src/api/admin/api.rs b/src/api/admin/api.rs index 4ec62aa9..0c2d31ab 100644 --- a/src/api/admin/api.rs +++ b/src/api/admin/api.rs @@ -180,9 +180,9 @@ pub struct NodeResp { pub is_up: bool, pub last_seen_secs_ago: Option, pub draining: bool, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub data_partition: Option, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub metadata_partition: Option, } @@ -272,7 +272,9 @@ pub struct GetClusterLayoutRequest; pub struct GetClusterLayoutResponse { pub version: u64, pub roles: Vec, + pub parameters: LayoutParameters, pub staged_role_changes: Vec, + pub staged_parameters: Option, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] @@ -303,10 +305,28 @@ pub enum NodeRoleChangeEnum { }, } +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct LayoutParameters { + pub zone_redundancy: ZoneRedundancy, +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub enum ZoneRedundancy { + AtLeast(usize), + Maximum, +} + // ---- UpdateClusterLayout ---- #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -pub struct UpdateClusterLayoutRequest(pub Vec); +pub struct UpdateClusterLayoutRequest { + #[serde(default)] + pub roles: Vec, + #[serde(default)] + pub parameters: Option, +} #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct UpdateClusterLayoutResponse(pub GetClusterLayoutResponse); @@ -367,7 +387,7 @@ pub struct GetKeyInfoRequest { pub struct GetKeyInfoResponse { pub name: String, pub access_key_id: String, - #[serde(skip_serializing_if = "is_default")] + #[serde(default, skip_serializing_if = "is_default")] pub secret_access_key: Option, pub permissions: KeyPerm, pub buckets: Vec, diff --git a/src/api/admin/cluster.rs b/src/api/admin/cluster.rs index 13946e2b..485979c4 100644 --- a/src/api/admin/cluster.rs +++ b/src/api/admin/cluster.rs @@ -218,10 +218,19 @@ fn format_cluster_layout(layout: &layout::LayoutHistory) -> GetClusterLayoutResp }) .collect::>(); + let staged_parameters = if *layout.staging.get().parameters.get() != layout.current().parameters + { + Some((*layout.staging.get().parameters.get()).into()) + } else { + None + }; + GetClusterLayoutResponse { version: layout.current().version, roles, + parameters: layout.current().parameters.into(), staged_role_changes, + staged_parameters, } } @@ -242,7 +251,7 @@ impl RequestHandler for UpdateClusterLayoutRequest { let mut roles = layout.current().roles.clone(); roles.merge(&layout.staging.get().roles); - for change in self.0 { + for change in self.roles { let node = hex::decode(&change.id).ok_or_bad_request("Invalid node identifier")?; let node = Uuid::try_from(&node).ok_or_bad_request("Invalid node identifier")?; @@ -252,11 +261,16 @@ impl RequestHandler for UpdateClusterLayoutRequest { zone, capacity, tags, - } => Some(layout::NodeRole { - zone, - capacity, - tags, - }), + } => { + if matches!(capacity, Some(cap) if cap < 1024) { + return Err(Error::bad_request("Capacity should be at least 1K (1024)")); + } + Some(layout::NodeRole { + zone, + capacity, + tags, + }) + } _ => return Err(Error::bad_request("Invalid layout change")), }; @@ -267,6 +281,22 @@ impl RequestHandler for UpdateClusterLayoutRequest { .merge(&roles.update_mutator(node, layout::NodeRoleV(new_role))); } + if let Some(param) = self.parameters { + if let ZoneRedundancy::AtLeast(r_int) = param.zone_redundancy { + if r_int > layout.current().replication_factor { + return Err(Error::bad_request(format!( + "The zone redundancy must be smaller or equal to the replication factor ({}).", + layout.current().replication_factor + ))); + } else if r_int < 1 { + return Err(Error::bad_request( + "The zone redundancy must be at least 1.", + )); + } + } + layout.staging.get_mut().parameters.update(param.into()); + } + garage .system .layout_manager @@ -322,3 +352,39 @@ impl RequestHandler for RevertClusterLayoutRequest { Ok(RevertClusterLayoutResponse(res)) } } + +// ---- + +impl From for ZoneRedundancy { + fn from(x: layout::ZoneRedundancy) -> Self { + match x { + layout::ZoneRedundancy::Maximum => ZoneRedundancy::Maximum, + layout::ZoneRedundancy::AtLeast(x) => ZoneRedundancy::AtLeast(x), + } + } +} + +impl Into for ZoneRedundancy { + fn into(self) -> layout::ZoneRedundancy { + match self { + ZoneRedundancy::Maximum => layout::ZoneRedundancy::Maximum, + ZoneRedundancy::AtLeast(x) => layout::ZoneRedundancy::AtLeast(x), + } + } +} + +impl From for LayoutParameters { + fn from(x: layout::LayoutParameters) -> Self { + LayoutParameters { + zone_redundancy: x.zone_redundancy.into(), + } + } +} + +impl Into for LayoutParameters { + fn into(self) -> layout::LayoutParameters { + layout::LayoutParameters { + zone_redundancy: self.zone_redundancy.into(), + } + } +} diff --git a/src/api/admin/router_v2.rs b/src/api/admin/router_v2.rs index 2c2067dc..2397f276 100644 --- a/src/api/admin/router_v2.rs +++ b/src/api/admin/router_v2.rs @@ -108,10 +108,7 @@ impl AdminApiRequest { Endpoint::GetClusterLayout => { Ok(AdminApiRequest::GetClusterLayout(GetClusterLayoutRequest)) } - Endpoint::UpdateClusterLayout => { - let updates = parse_json_body::(req).await?; - Ok(AdminApiRequest::UpdateClusterLayout(updates)) - } + // UpdateClusterLayout semantics changed Endpoint::ApplyClusterLayout => { let param = parse_json_body::(req).await?; Ok(AdminApiRequest::ApplyClusterLayout(param)) diff --git a/src/garage/cli/layout.rs b/src/garage/cli/layout.rs index bb77cc2a..c93e7a72 100644 --- a/src/garage/cli/layout.rs +++ b/src/garage/cli/layout.rs @@ -57,54 +57,6 @@ pub async fn cmd_show_layout( Ok(()) } -pub async fn cmd_config_layout( - rpc_cli: &Endpoint, - rpc_host: NodeID, - config_opt: ConfigLayoutOpt, -) -> Result<(), Error> { - let mut layout = fetch_layout(rpc_cli, rpc_host).await?; - - let mut did_something = false; - match config_opt.redundancy { - None => (), - Some(r_str) => { - let r = r_str - .parse::() - .ok_or_message("invalid zone redundancy value")?; - if let ZoneRedundancy::AtLeast(r_int) = r { - if r_int > layout.current().replication_factor { - return Err(Error::Message(format!( - "The zone redundancy must be smaller or equal to the \ - replication factor ({}).", - layout.current().replication_factor - ))); - } else if r_int < 1 { - return Err(Error::Message( - "The zone redundancy must be at least 1.".into(), - )); - } - } - - layout - .staging - .get_mut() - .parameters - .update(LayoutParameters { zone_redundancy: r }); - println!("The zone redundancy parameter has been set to '{}'.", r); - did_something = true; - } - } - - if !did_something { - return Err(Error::Message( - "Please specify an action for `garage layout config`".into(), - )); - } - - send_layout(rpc_cli, rpc_host, layout).await?; - Ok(()) -} - pub async fn cmd_layout_history( rpc_cli: &Endpoint, rpc_host: NodeID, diff --git a/src/garage/cli_v2/layout.rs b/src/garage/cli_v2/layout.rs index 2f14b332..40f3e924 100644 --- a/src/garage/cli_v2/layout.rs +++ b/src/garage/cli_v2/layout.rs @@ -4,6 +4,7 @@ use format_table::format_table; use garage_util::error::*; use garage_api_admin::api::*; +use garage_rpc::layout; use crate::cli::layout as cli_v1; use crate::cli::structs::*; @@ -14,6 +15,7 @@ impl Cli { match cmd { LayoutOperation::Assign(assign_opt) => self.cmd_assign_role(assign_opt).await, LayoutOperation::Remove(remove_opt) => self.cmd_remove_role(remove_opt).await, + LayoutOperation::Config(config_opt) => self.cmd_config_layout(config_opt).await, LayoutOperation::Apply(apply_opt) => self.cmd_apply_layout(apply_opt).await, LayoutOperation::Revert(revert_opt) => self.cmd_revert_layout(revert_opt).await, @@ -21,10 +23,6 @@ impl Cli { LayoutOperation::Show => { cli_v1::cmd_show_layout(&self.system_rpc_endpoint, self.rpc_host).await } - LayoutOperation::Config(config_opt) => { - cli_v1::cmd_config_layout(&self.system_rpc_endpoint, self.rpc_host, config_opt) - .await - } LayoutOperation::History => { cli_v1::cmd_layout_history(&self.system_rpc_endpoint, self.rpc_host).await } @@ -100,8 +98,11 @@ impl Cli { }); } - self.api_request(UpdateClusterLayoutRequest(actions)) - .await?; + self.api_request(UpdateClusterLayoutRequest { + roles: actions, + parameters: None, + }) + .await?; println!("Role changes are staged but not yet committed."); println!("Use `garage layout show` to view staged role changes,"); @@ -126,8 +127,11 @@ impl Cli { action: NodeRoleChangeEnum::Remove { remove: true }, }]; - self.api_request(UpdateClusterLayoutRequest(actions)) - .await?; + self.api_request(UpdateClusterLayoutRequest { + roles: actions, + parameters: None, + }) + .await?; println!("Role removal is staged but not yet committed."); println!("Use `garage layout show` to view staged role changes,"); @@ -135,6 +139,36 @@ impl Cli { Ok(()) } + pub async fn cmd_config_layout(&self, config_opt: ConfigLayoutOpt) -> Result<(), Error> { + let mut did_something = false; + match config_opt.redundancy { + None => (), + Some(r_str) => { + let r = r_str + .parse::() + .ok_or_message("invalid zone redundancy value")?; + + self.api_request(UpdateClusterLayoutRequest { + roles: vec![], + parameters: Some(LayoutParameters { + zone_redundancy: r.into(), + }), + }) + .await?; + println!("The zone redundancy parameter has been set to '{}'.", r); + did_something = true; + } + } + + if !did_something { + return Err(Error::Message( + "Please specify an action for `garage layout config`".into(), + )); + } + + Ok(()) + } + pub async fn cmd_apply_layout(&self, apply_opt: ApplyLayoutOpt) -> Result<(), Error> { let missing_version_error = r#" Please pass the new layout version number to ensure that you are writing the correct version of the cluster layout.