diff --git a/doc/drafts/admin-api.md b/doc/drafts/admin-api.md index cbd73e15..2e1ffa82 100644 --- a/doc/drafts/admin-api.md +++ b/doc/drafts/admin-api.md @@ -94,6 +94,36 @@ Example response body: } ``` +### ConnectClusterNodes `POST /v0/connect` + +Instructs this Garage node to connect to other Garage nodes at specified addresses. + +Example request body: + +```json +[ + "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f@10.0.0.11:3901", + "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff@10.0.0.12:3901" +] +``` + +The format of the string for a node to connect to is: `@:`, same as in the `garage node connect` CLI call. + +Example response: + +```json +[ + { + "success": true, + "error": null, + }, + { + "success": false, + "error": "Handshake error", + } +] +``` + ### GetClusterLayout `GET /v0/layout` Returns the cluster's current layout in JSON, including: diff --git a/src/api/admin/api_server.rs b/src/api/admin/api_server.rs index 6f568024..61b0d24f 100644 --- a/src/api/admin/api_server.rs +++ b/src/api/admin/api_server.rs @@ -124,6 +124,7 @@ impl ApiHandler for AdminApiServer { Endpoint::Options => self.handle_options(&req), Endpoint::Metrics => self.handle_metrics(), Endpoint::GetClusterStatus => handle_get_cluster_status(&self.garage).await, + Endpoint::ConnectClusterNodes => handle_connect_cluster_nodes(&self.garage, req).await, // Layout Endpoint::GetClusterLayout => handle_get_cluster_layout(&self.garage).await, Endpoint::UpdateClusterLayout => handle_update_cluster_layout(&self.garage, req).await, diff --git a/src/api/admin/cluster.rs b/src/api/admin/cluster.rs index 44ad4a37..3401be42 100644 --- a/src/api/admin/cluster.rs +++ b/src/api/admin/cluster.rs @@ -45,6 +45,33 @@ pub async fn handle_get_cluster_status(garage: &Arc) -> Result, + req: Request, +) -> Result, Error> { + let req = parse_json_body::>(req).await?; + + let res = futures::future::join_all(req.iter().map(|node| garage.system.connect(node))) + .await + .into_iter() + .map(|r| match r { + Ok(()) => ConnectClusterNodesResponse { + success: true, + error: None, + }, + Err(e) => ConnectClusterNodesResponse { + success: false, + error: Some(format!("{}", e)), + }, + }) + .collect::>(); + + let resp_json = serde_json::to_string_pretty(&res).map_err(GarageError::from)?; + Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::from(resp_json))?) +} + pub async fn handle_get_cluster_layout(garage: &Arc) -> Result, Error> { let res = get_cluster_layout(garage); let resp_json = serde_json::to_string_pretty(&res).map_err(GarageError::from)?; @@ -84,6 +111,12 @@ struct GetClusterStatusResponse { layout: GetClusterLayoutResponse, } +#[derive(Serialize)] +struct ConnectClusterNodesResponse { + success: bool, + error: Option, +} + #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct GetClusterLayoutResponse { diff --git a/src/api/admin/router.rs b/src/api/admin/router.rs index 909ef102..41e7ed73 100644 --- a/src/api/admin/router.rs +++ b/src/api/admin/router.rs @@ -18,6 +18,7 @@ pub enum Endpoint { Options, Metrics, GetClusterStatus, + ConnectClusterNodes, // Layout GetClusterLayout, UpdateClusterLayout, @@ -91,6 +92,7 @@ impl Endpoint { OPTIONS _ => Options, GET "/metrics" => Metrics, GET "/v0/status" => GetClusterStatus, + POST "/v0/connect" => ConnectClusterNodes, // Layout endpoints GET "/v0/layout" => GetClusterLayout, POST "/v0/layout" => UpdateClusterLayout, diff --git a/src/rpc/system.rs b/src/rpc/system.rs index eb2f2e42..78d538ec 100644 --- a/src/rpc/system.rs +++ b/src/rpc/system.rs @@ -383,10 +383,14 @@ impl System { } } } - return Err(Error::Message(format!( - "Could not connect to specified peers. Errors: {:?}", - errors - ))); + if errors.len() == 1 { + return Err(Error::Message(errors[0].1.to_string())); + } else { + return Err(Error::Message(format!( + "Could not connect to specified peers. Errors: {:?}", + errors + ))); + } } // ---- INTERNALS ----