admin api: change cluster status/layout to use lists and not maps (fix #377)

This commit is contained in:
Alex Auvolat 2023-06-14 13:45:27 +02:00
parent 187240e539
commit 52376d47ca
3 changed files with 175 additions and 75 deletions

View file

@ -56,7 +56,7 @@ See `/v0/health` for an API that also returns JSON output.
### Cluster operations ### Cluster operations
#### GetClusterStatus `GET /v0/status` #### GetClusterStatus `GET /v1/status`
Returns the cluster's current status in JSON, including: Returns the cluster's current status in JSON, including:
@ -70,67 +70,93 @@ Example response body:
```json ```json
{ {
"node": "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f", "node": "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f",
"garage_version": "git:v0.8.0", "garageVersion": "git:v0.9.0-dev",
"knownNodes": { "garageFeatures": [
"ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f": { "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", "addr": "10.0.0.11:3901",
"is_up": true, "is_up": true,
"last_seen_secs_ago": 9, "last_seen_secs_ago": 9,
"hostname": "node1" "hostname": "node1"
}, },
"4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff": { {
"id": "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff",
"addr": "10.0.0.12:3901", "addr": "10.0.0.12:3901",
"is_up": true, "is_up": true,
"last_seen_secs_ago": 1, "last_seen_secs_ago": 1,
"hostname": "node2" "hostname": "node2"
}, },
"23ffd0cdd375ebff573b20cc5cef38996b51c1a7d6dbcf2c6e619876e507cf27": { {
"id": "23ffd0cdd375ebff573b20cc5cef38996b51c1a7d6dbcf2c6e619876e507cf27",
"addr": "10.0.0.21:3901", "addr": "10.0.0.21:3901",
"is_up": true, "is_up": true,
"last_seen_secs_ago": 7, "last_seen_secs_ago": 7,
"hostname": "node3" "hostname": "node3"
}, },
"e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b": { {
"id": "e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b",
"addr": "10.0.0.22:3901", "addr": "10.0.0.22:3901",
"is_up": true, "is_up": true,
"last_seen_secs_ago": 1, "last_seen_secs_ago": 1,
"hostname": "node4" "hostname": "node4"
} }
}, ],
"layout": { "layout": {
"version": 12, "version": 12,
"roles": { "roles": [
"ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f": { {
"id": "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f",
"zone": "dc1", "zone": "dc1",
"capacity": 4, "capacity": 10737418240,
"tags": [ "tags": [
"node1" "node1"
] ]
}, },
"4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff": { {
"id": "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff",
"zone": "dc1", "zone": "dc1",
"capacity": 6, "capacity": 10737418240,
"tags": [ "tags": [
"node2" "node2"
] ]
}, },
"23ffd0cdd375ebff573b20cc5cef38996b51c1a7d6dbcf2c6e619876e507cf27": { {
"id": "23ffd0cdd375ebff573b20cc5cef38996b51c1a7d6dbcf2c6e619876e507cf27",
"zone": "dc2", "zone": "dc2",
"capacity": 10, "capacity": 10737418240,
"tags": [ "tags": [
"node3" "node3"
] ]
} }
}, ],
"stagedRoleChanges": { "stagedRoleChanges": [
"e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b": { {
"id": "e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b",
"remove": false,
"zone": "dc2", "zone": "dc2",
"capacity": 5, "capacity": 10737418240,
"tags": [ "tags": [
"node4" "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: Returns the cluster's current layout in JSON, including:
@ -212,42 +238,54 @@ Example response body:
```json ```json
{ {
"version": 12, "version": 12,
"roles": { "roles": [
"ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f": { {
"id": "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f",
"zone": "dc1", "zone": "dc1",
"capacity": 4, "capacity": 10737418240,
"tags": [ "tags": [
"node1" "node1"
] ]
}, },
"4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff": { {
"id": "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff",
"zone": "dc1", "zone": "dc1",
"capacity": 6, "capacity": 10737418240,
"tags": [ "tags": [
"node2" "node2"
] ]
}, },
"23ffd0cdd375ebff573b20cc5cef38996b51c1a7d6dbcf2c6e619876e507cf27": { {
"id": "23ffd0cdd375ebff573b20cc5cef38996b51c1a7d6dbcf2c6e619876e507cf27",
"zone": "dc2", "zone": "dc2",
"capacity": 10, "capacity": 10737418240,
"tags": [ "tags": [
"node3" "node3"
] ]
} }
}, ],
"stagedRoleChanges": { "stagedRoleChanges": [
"e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b": { {
"id": "e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b",
"remove": false,
"zone": "dc2", "zone": "dc2",
"capacity": 5, "capacity": 10737418240,
"tags": [ "tags": [
"node4" "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 Send modifications to the cluster layout. These modifications will
be included in the staged role changes, visible in subsequent calls be included in the staged role changes, visible in subsequent calls
@ -259,8 +297,9 @@ the layout.
Request body format: Request body format:
```json ```json
{ [
<node_id>: { {
"id": <node_id>,
"capacity": <new_capacity>, "capacity": <new_capacity>,
"zone": <new_zone>, "zone": <new_zone>,
"tags": [ "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 Contrary to the CLI that may update only a subset of the fields

View file

@ -1,4 +1,3 @@
use std::collections::HashMap;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
@ -8,7 +7,7 @@ use serde::{Deserialize, Serialize};
use garage_util::crdt::*; use garage_util::crdt::*;
use garage_util::data::*; use garage_util::data::*;
use garage_rpc::layout::*; use garage_rpc::layout;
use garage_model::garage::Garage; use garage_model::garage::Garage;
@ -26,16 +25,12 @@ pub async fn handle_get_cluster_status(garage: &Arc<Garage>) -> Result<Response<
.system .system
.get_known_nodes() .get_known_nodes()
.into_iter() .into_iter()
.map(|i| { .map(|i| KnownNodeResp {
( id: hex::encode(i.id),
hex::encode(i.id), addr: i.addr,
KnownNodeResp { is_up: i.is_up,
addr: i.addr, last_seen_secs_ago: i.last_seen_secs_ago,
is_up: i.is_up, hostname: i.status.hostname,
last_seen_secs_ago: i.last_seen_secs_ago,
hostname: i.status.hostname,
},
)
}) })
.collect(), .collect(),
layout: get_cluster_layout(garage), 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 { fn get_cluster_layout(garage: &Arc<Garage>) -> GetClusterLayoutResponse {
let layout = garage.system.get_cluster_layout(); 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 { GetClusterLayoutResponse {
version: layout.version, version: layout.version,
roles: layout roles,
.roles staged_role_changes,
.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(),
} }
} }
// ----
#[derive(Serialize)] #[derive(Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct GetClusterStatusResponse { struct GetClusterStatusResponse {
@ -109,7 +128,7 @@ struct GetClusterStatusResponse {
garage_features: Option<&'static [&'static str]>, garage_features: Option<&'static [&'static str]>,
rust_version: &'static str, rust_version: &'static str,
db_engine: String, db_engine: String,
known_nodes: HashMap<String, KnownNodeResp>, known_nodes: Vec<KnownNodeResp>,
layout: GetClusterLayoutResponse, layout: GetClusterLayoutResponse,
} }
@ -124,19 +143,31 @@ struct ConnectClusterNodesResponse {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct GetClusterLayoutResponse { struct GetClusterLayoutResponse {
version: u64, version: u64,
roles: HashMap<String, Option<NodeRole>>, roles: Vec<NodeRoleResp>,
staged_role_changes: HashMap<String, Option<NodeRole>>, 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)] #[derive(Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct KnownNodeResp { struct KnownNodeResp {
id: String,
addr: SocketAddr, addr: SocketAddr,
is_up: bool, is_up: bool,
last_seen_secs_ago: Option<u64>, last_seen_secs_ago: Option<u64>,
hostname: String, hostname: String,
} }
// ---- update functions ----
pub async fn handle_update_cluster_layout( pub async fn handle_update_cluster_layout(
garage: &Arc<Garage>, garage: &Arc<Garage>,
req: Request<Body>, req: Request<Body>,
@ -148,13 +179,23 @@ pub async fn handle_update_cluster_layout(
let mut roles = layout.roles.clone(); let mut roles = layout.roles.clone();
roles.merge(&layout.staging_roles); roles.merge(&layout.staging_roles);
for (node, role) in updates { for change in updates {
let node = hex::decode(node).ok_or_bad_request("Invalid node identifier")?; 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 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 layout
.staging_roles .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?; garage.system.update_cluster_layout(&layout).await?;
@ -196,10 +237,28 @@ pub async fn handle_revert_cluster_layout(
.body(Body::empty())?) .body(Body::empty())?)
} }
type UpdateClusterLayoutRequest = HashMap<String, Option<NodeRole>>; // ----
type UpdateClusterLayoutRequest = Vec<NodeRoleChange>;
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct ApplyRevertLayoutRequest { struct ApplyRevertLayoutRequest {
version: u64, 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>>,
}

View file

@ -95,12 +95,12 @@ impl Endpoint {
GET "/check" => CheckWebsiteEnabled, GET "/check" => CheckWebsiteEnabled,
GET "/health" => Health, GET "/health" => Health,
GET "/metrics" => Metrics, GET "/metrics" => Metrics,
GET "/v0/status" => GetClusterStatus, GET "/v1/status" => GetClusterStatus,
GET "/v0/health" => GetClusterHealth, GET "/v0/health" => GetClusterHealth,
POST "/v0/connect" => ConnectClusterNodes, POST "/v0/connect" => ConnectClusterNodes,
// Layout endpoints // Layout endpoints
GET "/v0/layout" => GetClusterLayout, GET "/v1/layout" => GetClusterLayout,
POST "/v0/layout" => UpdateClusterLayout, POST "/v1/layout" => UpdateClusterLayout,
POST "/v0/layout/apply" => ApplyClusterLayout, POST "/v0/layout/apply" => ApplyClusterLayout,
POST "/v0/layout/revert" => RevertClusterLayout, POST "/v0/layout/revert" => RevertClusterLayout,
// API key endpoints // API key endpoints