diff --git a/doc/api/garage-admin-v2.yml b/doc/api/garage-admin-v2.yml index f9e3c10c..739b4166 100644 --- a/doc/api/garage-admin-v2.yml +++ b/doc/api/garage-admin-v2.yml @@ -84,34 +84,12 @@ paths: application/json: schema: type: object - required: [ node, garageVersion, garageFeatures, rustVersion, dbEngine, knownNodes, layout ] + required: [ layoutVersion, nodes ] properties: - node: - type: string - example: "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f" - garageVersion: - type: string - example: "v2.0.0" - garageFeatures: - type: array - items: - type: string - example: - - "k2v" - - "lmdb" - - "sqlite" - - "consul-discovery" - - "kubernetes-discovery" - - "metrics" - - "telemetry-otlp" - - "bundled-libs" - rustVersion: - type: string - example: "1.68.0" - dbEngine: - type: string - example: "LMDB (using Heed crate)" - knownNodes: + layoutVersion: + type: integer + example: 1 + nodes: type: array example: - id: "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f" @@ -131,8 +109,6 @@ paths: hostname: neptune items: $ref: '#/components/schemas/NodeNetworkInfo' - layout: - $ref: '#/components/schemas/ClusterLayout' /ConnectClusterNodes: post: diff --git a/doc/drafts/admin-api.md b/doc/drafts/admin-api.md index 029c7ddd..0613c535 100644 --- a/doc/drafts/admin-api.md +++ b/doc/drafts/admin-api.md @@ -68,26 +68,13 @@ Returns HTTP 200 Ok if yes, or HTTP 4xx if no website is available for this doma Returns the cluster's current status in JSON, 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 Example response body: ```json { - "node": "b10c110e4e854e5aa3f4637681befac755154b20059ec163254ddbfae86b09df", - "garageVersion": "v2.0.0", - "garageFeatures": [ - "k2v", - "lmdb", - "sqlite", - "metrics", - "bundled-libs" - ], - "rustVersion": "1.68.0", - "dbEngine": "LMDB (using Heed crate)", "layoutVersion": 5, "nodes": [ { @@ -362,19 +349,7 @@ layout, as well as the description of the layout as returned by GetClusterLayout Clears all of the staged layout changes. -Request body format: - -```json -{ - "version": 13 -} -``` - -Reverting the staged changes is done by incrementing the version number -and clearing the contents of the staged change list. -Similarly to the CLI, the body must include the incremented -version number, which MUST be 1 + the value of the currently -existing layout in the cluster. +This requests contains an empty body. This returns the new cluster layout with all changes reverted, as returned by GetClusterLayout. diff --git a/src/api/admin/api.rs b/src/api/admin/api.rs index 97cde158..4ab28e2d 100644 --- a/src/api/admin/api.rs +++ b/src/api/admin/api.rs @@ -10,10 +10,9 @@ use garage_rpc::*; use garage_model::garage::Garage; -use garage_api_common::common_error::CommonErrorDerivative; use garage_api_common::helpers::is_default; -use crate::api_server::{AdminRpc, AdminRpcResponse}; +use crate::api_server::{find_matching_nodes, AdminRpc, AdminRpcResponse}; use crate::error::Error; use crate::macros::*; use crate::{Admin, RequestHandler}; @@ -77,6 +76,7 @@ admin_endpoints![ RemoveBucketAlias, // Node operations + GetNodeInfo, CreateMetadataSnapshot, GetNodeStatistics, GetClusterStatistics, @@ -97,6 +97,7 @@ admin_endpoints![ local_admin_endpoints![ // Node operations + GetNodeInfo, CreateMetadataSnapshot, GetNodeStatistics, LaunchRepairOperation, @@ -157,11 +158,6 @@ pub struct GetClusterStatusRequest; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetClusterStatusResponse { - pub node: String, - pub garage_version: String, - pub garage_features: Option>, - pub rust_version: String, - pub db_engine: String, pub layout_version: u64, pub nodes: Vec, } @@ -636,6 +632,21 @@ pub struct RemoveBucketAliasResponse(pub GetBucketInfoResponse); // Node operations // ********************************************** +// ---- GetNodeInfo ---- + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct LocalGetNodeInfoRequest; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct LocalGetNodeInfoResponse { + pub node_id: String, + pub garage_version: String, + pub garage_features: Option>, + pub rust_version: String, + pub db_engine: String, +} + // ---- CreateMetadataSnapshot ---- #[derive(Debug, Clone, Serialize, Deserialize, Default)] diff --git a/src/api/admin/api_server.rs b/src/api/admin/api_server.rs index 37574dcf..0e6afce2 100644 --- a/src/api/admin/api_server.rs +++ b/src/api/admin/api_server.rs @@ -16,6 +16,7 @@ use opentelemetry_prometheus::PrometheusExporter; use garage_model::garage::Garage; use garage_rpc::{Endpoint as RpcEndpoint, *}; use garage_util::background::BackgroundRunner; +use garage_util::data::Uuid; use garage_util::error::Error as GarageError; use garage_util::socket_address::UnixOrTCPSocketAddress; @@ -265,3 +266,40 @@ fn verify_bearer_token(token: &hyper::http::HeaderValue, password_hash: &str) -> Ok(()) } + +pub(crate) fn find_matching_nodes(garage: &Garage, spec: &str) -> Result, Error> { + let mut res = vec![]; + if spec == "*" { + res = garage.system.cluster_layout().all_nodes().to_vec(); + for node in garage.system.get_known_nodes() { + if node.is_up && !res.contains(&node.id) { + res.push(node.id); + } + } + } else if spec == "self" { + res.push(garage.system.id); + } else { + let layout = garage.system.cluster_layout(); + let known_nodes = garage.system.get_known_nodes(); + let all_nodes = layout + .all_nodes() + .iter() + .copied() + .chain(known_nodes.iter().filter(|x| x.is_up).map(|x| x.id)); + for node in all_nodes { + if !res.contains(&node) && hex::encode(node).starts_with(spec) { + res.push(node); + } + } + if res.is_empty() { + return Err(Error::bad_request(format!("No nodes matching {}", spec))); + } + if res.len() > 1 { + return Err(Error::bad_request(format!( + "Multiple nodes matching {}: {:?}", + spec, res + ))); + } + } + Ok(res) +} diff --git a/src/api/admin/cluster.rs b/src/api/admin/cluster.rs index cb1fa493..13946e2b 100644 --- a/src/api/admin/cluster.rs +++ b/src/api/admin/cluster.rs @@ -105,12 +105,6 @@ impl RequestHandler for GetClusterStatusRequest { nodes.sort_by(|x, y| x.id.cmp(&y.id)); Ok(GetClusterStatusResponse { - node: hex::encode(garage.system.id), - garage_version: garage_util::version::garage_version().to_string(), - garage_features: garage_util::version::garage_features() - .map(|features| features.iter().map(ToString::to_string).collect()), - rust_version: garage_util::version::rust_version().to_string(), - db_engine: garage.db.engine(), layout_version: layout.current().version, nodes, }) diff --git a/src/api/admin/macros.rs b/src/api/admin/macros.rs index df2762fe..bf841295 100644 --- a/src/api/admin/macros.rs +++ b/src/api/admin/macros.rs @@ -136,20 +136,7 @@ macro_rules! local_admin_endpoints { type Response = [< $endpoint Response >]; async fn handle(self, garage: &Arc, admin: &Admin) -> Result { - let to = match self.node.as_str() { - "*" => garage.system.cluster_layout().all_nodes().to_vec(), - id => { - let nodes = garage.system.cluster_layout().all_nodes() - .iter() - .filter(|x| hex::encode(x).starts_with(id)) - .cloned() - .collect::>(); - if nodes.len() != 1 { - return Err(Error::bad_request(format!("Zero or multiple nodes matching {}: {:?}", id, nodes))); - } - nodes - } - }; + let to = find_matching_nodes(garage, self.node.as_str())?; let resps = garage.system.rpc_helper().call_many(&admin.endpoint, &to, diff --git a/src/api/admin/node.rs b/src/api/admin/node.rs index f6f43d95..3c7b5c03 100644 --- a/src/api/admin/node.rs +++ b/src/api/admin/node.rs @@ -18,6 +18,25 @@ use crate::api::*; use crate::error::Error; use crate::{Admin, RequestHandler}; +impl RequestHandler for LocalGetNodeInfoRequest { + type Response = LocalGetNodeInfoResponse; + + async fn handle( + self, + garage: &Arc, + _admin: &Admin, + ) -> Result { + Ok(LocalGetNodeInfoResponse { + node_id: hex::encode(garage.system.id), + garage_version: garage_util::version::garage_version().to_string(), + garage_features: garage_util::version::garage_features() + .map(|features| features.iter().map(ToString::to_string).collect()), + rust_version: garage_util::version::rust_version().to_string(), + db_engine: garage.db.engine(), + }) + } +} + impl RequestHandler for LocalCreateMetadataSnapshotRequest { type Response = LocalCreateMetadataSnapshotResponse; diff --git a/src/api/admin/router_v2.rs b/src/api/admin/router_v2.rs index 4d5c015e..2c2067dc 100644 --- a/src/api/admin/router_v2.rs +++ b/src/api/admin/router_v2.rs @@ -60,6 +60,7 @@ impl AdminApiRequest { POST AddBucketAlias (body), POST RemoveBucketAlias (body), // Node APIs + GET GetNodeInfo (default::body, query::node), POST CreateMetadataSnapshot (default::body, query::node), GET GetNodeStatistics (default::body, query::node), GET GetClusterStatistics (), @@ -93,9 +94,8 @@ impl AdminApiRequest { use router_v1::Endpoint; match v1_endpoint { - Endpoint::GetClusterStatus => { - Ok(AdminApiRequest::GetClusterStatus(GetClusterStatusRequest)) - } + // GetClusterStatus semantics changed: + // info about local node is no longer returned Endpoint::GetClusterHealth => { Ok(AdminApiRequest::GetClusterHealth(GetClusterHealthRequest)) }