aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Auvolat <alex@adnab.me>2023-06-14 13:45:27 +0200
committerAlex Auvolat <alex@adnab.me>2023-06-14 13:45:27 +0200
commit52376d47caf747f5cf93a21e5c15e4e6b8d991ee (patch)
tree68ce9c2267aa68cab578e541089515df2f0cbc27
parent187240e539f30036eb76785227ee8095ef4dd355 (diff)
downloadgarage-52376d47caf747f5cf93a21e5c15e4e6b8d991ee.tar.gz
garage-52376d47caf747f5cf93a21e5c15e4e6b8d991ee.zip
admin api: change cluster status/layout to use lists and not maps (fix #377)
-rw-r--r--doc/drafts/admin-api.md119
-rw-r--r--src/api/admin/cluster.rs125
-rw-r--r--src/api/admin/router.rs6
3 files changed, 175 insertions, 75 deletions
diff --git a/doc/drafts/admin-api.md b/doc/drafts/admin-api.md
index e0252f71..b1a8f402 100644
--- a/doc/drafts/admin-api.md
+++ b/doc/drafts/admin-api.md
@@ -56,7 +56,7 @@ See `/v0/health` for an API that also returns JSON output.
### Cluster operations
-#### GetClusterStatus `GET /v0/status`
+#### GetClusterStatus `GET /v1/status`
Returns the cluster's current status in JSON, including:
@@ -70,67 +70,93 @@ Example response body:
```json
{
"node": "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f",
- "garage_version": "git:v0.8.0",
- "knownNodes": {
- "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f": {
+ "garageVersion": "git:v0.9.0-dev",
+ "garageFeatures": [
+ "k2v",
+ "sled",
+ "lmdb",
+ "sqlite",
+ "metrics",
+ "bundled-libs"
+ ],
+ "rustVersion": "1.68.0",
+ "dbEngine": "LMDB (using Heed crate)",
+ "knownNodes": [
+ {
+ "id": "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f",
"addr": "10.0.0.11:3901",
"is_up": true,
"last_seen_secs_ago": 9,
"hostname": "node1"
},
- "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff": {
+ {
+ "id": "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff",
"addr": "10.0.0.12:3901",
"is_up": true,
"last_seen_secs_ago": 1,
"hostname": "node2"
},
- "23ffd0cdd375ebff573b20cc5cef38996b51c1a7d6dbcf2c6e619876e507cf27": {
+ {
+ "id": "23ffd0cdd375ebff573b20cc5cef38996b51c1a7d6dbcf2c6e619876e507cf27",
"addr": "10.0.0.21:3901",
"is_up": true,
"last_seen_secs_ago": 7,
"hostname": "node3"
},
- "e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b": {
+ {
+ "id": "e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b",
"addr": "10.0.0.22:3901",
"is_up": true,
"last_seen_secs_ago": 1,
"hostname": "node4"
}
- },
+ ],
"layout": {
"version": 12,
- "roles": {
- "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f": {
+ "roles": [
+ {
+ "id": "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f",
"zone": "dc1",
- "capacity": 4,
+ "capacity": 10737418240,
"tags": [
"node1"
]
},
- "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff": {
+ {
+ "id": "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff",
"zone": "dc1",
- "capacity": 6,
+ "capacity": 10737418240,
"tags": [
"node2"
]
},
- "23ffd0cdd375ebff573b20cc5cef38996b51c1a7d6dbcf2c6e619876e507cf27": {
+ {
+ "id": "23ffd0cdd375ebff573b20cc5cef38996b51c1a7d6dbcf2c6e619876e507cf27",
"zone": "dc2",
- "capacity": 10,
+ "capacity": 10737418240,
"tags": [
"node3"
]
}
- },
- "stagedRoleChanges": {
- "e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b": {
+ ],
+ "stagedRoleChanges": [
+ {
+ "id": "e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b",
+ "remove": false,
"zone": "dc2",
- "capacity": 5,
+ "capacity": 10737418240,
"tags": [
"node4"
]
}
- }
+ {
+ "id": "23ffd0cdd375ebff573b20cc5cef38996b51c1a7d6dbcf2c6e619876e507cf27",
+ "remove": true,
+ "zone": null,
+ "capacity": null,
+ "tags": null,
+ }
+ ]
}
}
```
@@ -198,7 +224,7 @@ Example response:
]
```
-#### GetClusterLayout `GET /v0/layout`
+#### GetClusterLayout `GET /v1/layout`
Returns the cluster's current layout in JSON, including:
@@ -212,42 +238,54 @@ Example response body:
```json
{
"version": 12,
- "roles": {
- "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f": {
+ "roles": [
+ {
+ "id": "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f",
"zone": "dc1",
- "capacity": 4,
+ "capacity": 10737418240,
"tags": [
"node1"
]
},
- "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff": {
+ {
+ "id": "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff",
"zone": "dc1",
- "capacity": 6,
+ "capacity": 10737418240,
"tags": [
"node2"
]
},
- "23ffd0cdd375ebff573b20cc5cef38996b51c1a7d6dbcf2c6e619876e507cf27": {
+ {
+ "id": "23ffd0cdd375ebff573b20cc5cef38996b51c1a7d6dbcf2c6e619876e507cf27",
"zone": "dc2",
- "capacity": 10,
+ "capacity": 10737418240,
"tags": [
"node3"
]
}
- },
- "stagedRoleChanges": {
- "e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b": {
+ ],
+ "stagedRoleChanges": [
+ {
+ "id": "e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b",
+ "remove": false,
"zone": "dc2",
- "capacity": 5,
+ "capacity": 10737418240,
"tags": [
"node4"
]
}
- }
+ {
+ "id": "23ffd0cdd375ebff573b20cc5cef38996b51c1a7d6dbcf2c6e619876e507cf27",
+ "remove": true,
+ "zone": null,
+ "capacity": null,
+ "tags": null,
+ }
+ ]
}
```
-#### UpdateClusterLayout `POST /v0/layout`
+#### UpdateClusterLayout `POST /v1/layout`
Send modifications to the cluster layout. These modifications will
be included in the staged role changes, visible in subsequent calls
@@ -259,8 +297,9 @@ the layout.
Request body format:
```json
-{
- <node_id>: {
+[
+ {
+ "id": <node_id>,
"capacity": <new_capacity>,
"zone": <new_zone>,
"tags": [
@@ -268,9 +307,11 @@ Request body format:
...
]
},
- <node_id_to_remove>: null,
- ...
-}
+ {
+ "id": <node_id_to_remove>,
+ "remove": true
+ }
+]
```
Contrary to the CLI that may update only a subset of the fields
diff --git a/src/api/admin/cluster.rs b/src/api/admin/cluster.rs
index a2c97ee5..8a208a2c 100644
--- a/src/api/admin/cluster.rs
+++ b/src/api/admin/cluster.rs
@@ -1,4 +1,3 @@
-use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
@@ -8,7 +7,7 @@ use serde::{Deserialize, Serialize};
use garage_util::crdt::*;
use garage_util::data::*;
-use garage_rpc::layout::*;
+use garage_rpc::layout;
use garage_model::garage::Garage;
@@ -26,16 +25,12 @@ pub async fn handle_get_cluster_status(garage: &Arc<Garage>) -> Result<Response<
.system
.get_known_nodes()
.into_iter()
- .map(|i| {
- (
- hex::encode(i.id),
- KnownNodeResp {
- addr: i.addr,
- is_up: i.is_up,
- last_seen_secs_ago: i.last_seen_secs_ago,
- hostname: i.status.hostname,
- },
- )
+ .map(|i| KnownNodeResp {
+ id: hex::encode(i.id),
+ addr: i.addr,
+ is_up: i.is_up,
+ last_seen_secs_ago: i.last_seen_secs_ago,
+ hostname: i.status.hostname,
})
.collect(),
layout: get_cluster_layout(garage),
@@ -82,25 +77,49 @@ pub async fn handle_get_cluster_layout(garage: &Arc<Garage>) -> Result<Response<
fn get_cluster_layout(garage: &Arc<Garage>) -> GetClusterLayoutResponse {
let layout = garage.system.get_cluster_layout();
+ let roles = layout
+ .roles
+ .items()
+ .iter()
+ .filter_map(|(k, _, v)| v.0.clone().map(|x| (k, x)))
+ .map(|(k, v)| NodeRoleResp {
+ id: hex::encode(k),
+ zone: v.zone.clone(),
+ capacity: v.capacity,
+ tags: v.tags.clone(),
+ })
+ .collect::<Vec<_>>();
+
+ let staged_role_changes = layout
+ .staging_roles
+ .items()
+ .iter()
+ .filter(|(k, _, v)| layout.roles.get(k) != Some(v))
+ .map(|(k, _, v)| match &v.0 {
+ None => NodeRoleChange {
+ id: hex::encode(k),
+ remove: true,
+ ..Default::default()
+ },
+ Some(r) => NodeRoleChange {
+ id: hex::encode(k),
+ remove: false,
+ zone: Some(r.zone.clone()),
+ capacity: r.capacity,
+ tags: Some(r.tags.clone()),
+ },
+ })
+ .collect::<Vec<_>>();
+
GetClusterLayoutResponse {
version: layout.version,
- roles: layout
- .roles
- .items()
- .iter()
- .filter(|(_, _, v)| v.0.is_some())
- .map(|(k, _, v)| (hex::encode(k), v.0.clone()))
- .collect(),
- staged_role_changes: layout
- .staging_roles
- .items()
- .iter()
- .filter(|(k, _, v)| layout.roles.get(k) != Some(v))
- .map(|(k, _, v)| (hex::encode(k), v.0.clone()))
- .collect(),
+ roles,
+ staged_role_changes,
}
}
+// ----
+
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct GetClusterStatusResponse {
@@ -109,7 +128,7 @@ struct GetClusterStatusResponse {
garage_features: Option<&'static [&'static str]>,
rust_version: &'static str,
db_engine: String,
- known_nodes: HashMap<String, KnownNodeResp>,
+ known_nodes: Vec<KnownNodeResp>,
layout: GetClusterLayoutResponse,
}
@@ -124,19 +143,31 @@ struct ConnectClusterNodesResponse {
#[serde(rename_all = "camelCase")]
struct GetClusterLayoutResponse {
version: u64,
- roles: HashMap<String, Option<NodeRole>>,
- staged_role_changes: HashMap<String, Option<NodeRole>>,
+ roles: Vec<NodeRoleResp>,
+ staged_role_changes: Vec<NodeRoleChange>,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+struct NodeRoleResp {
+ id: String,
+ zone: String,
+ capacity: Option<u64>,
+ tags: Vec<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct KnownNodeResp {
+ id: String,
addr: SocketAddr,
is_up: bool,
last_seen_secs_ago: Option<u64>,
hostname: String,
}
+// ---- update functions ----
+
pub async fn handle_update_cluster_layout(
garage: &Arc<Garage>,
req: Request<Body>,
@@ -148,13 +179,23 @@ pub async fn handle_update_cluster_layout(
let mut roles = layout.roles.clone();
roles.merge(&layout.staging_roles);
- for (node, role) in updates {
- let node = hex::decode(node).ok_or_bad_request("Invalid node identifier")?;
+ for change in updates {
+ 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")?;
+ let new_role = match (change.remove, change.zone, change.capacity, change.tags) {
+ (true, None, None, None) => None,
+ (false, Some(zone), capacity, Some(tags)) => Some(layout::NodeRole {
+ zone,
+ capacity,
+ tags,
+ }),
+ _ => return Err(Error::bad_request("Invalid layout change")),
+ };
+
layout
.staging_roles
- .merge(&roles.update_mutator(node, NodeRoleV(role)));
+ .merge(&roles.update_mutator(node, layout::NodeRoleV(new_role)));
}
garage.system.update_cluster_layout(&layout).await?;
@@ -196,10 +237,28 @@ pub async fn handle_revert_cluster_layout(
.body(Body::empty())?)
}
-type UpdateClusterLayoutRequest = HashMap<String, Option<NodeRole>>;
+// ----
+
+type UpdateClusterLayoutRequest = Vec<NodeRoleChange>;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ApplyRevertLayoutRequest {
version: u64,
}
+
+// ----
+
+#[derive(Serialize, Deserialize, Default)]
+#[serde(rename_all = "camelCase")]
+struct NodeRoleChange {
+ id: String,
+ #[serde(default)]
+ remove: bool,
+ #[serde(default)]
+ zone: Option<String>,
+ #[serde(default)]
+ capacity: Option<u64>,
+ #[serde(default)]
+ tags: Option<Vec<String>>,
+}
diff --git a/src/api/admin/router.rs b/src/api/admin/router.rs
index 0dcb1546..5af3ffb5 100644
--- a/src/api/admin/router.rs
+++ b/src/api/admin/router.rs
@@ -95,12 +95,12 @@ impl Endpoint {
GET "/check" => CheckWebsiteEnabled,
GET "/health" => Health,
GET "/metrics" => Metrics,
- GET "/v0/status" => GetClusterStatus,
+ GET "/v1/status" => GetClusterStatus,
GET "/v0/health" => GetClusterHealth,
POST "/v0/connect" => ConnectClusterNodes,
// Layout endpoints
- GET "/v0/layout" => GetClusterLayout,
- POST "/v0/layout" => UpdateClusterLayout,
+ GET "/v1/layout" => GetClusterLayout,
+ POST "/v1/layout" => UpdateClusterLayout,
POST "/v0/layout/apply" => ApplyClusterLayout,
POST "/v0/layout/revert" => RevertClusterLayout,
// API key endpoints