admin api: change cluster status/layout to use lists and not maps (fix #377)
This commit is contained in:
parent
187240e539
commit
52376d47ca
3 changed files with 175 additions and 75 deletions
|
@ -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
|
||||||
|
|
|
@ -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),
|
|
||||||
KnownNodeResp {
|
|
||||||
addr: i.addr,
|
addr: i.addr,
|
||||||
is_up: i.is_up,
|
is_up: i.is_up,
|
||||||
last_seen_secs_ago: i.last_seen_secs_ago,
|
last_seen_secs_ago: i.last_seen_secs_ago,
|
||||||
hostname: i.status.hostname,
|
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();
|
||||||
|
|
||||||
GetClusterLayoutResponse {
|
let roles = layout
|
||||||
version: layout.version,
|
|
||||||
roles: layout
|
|
||||||
.roles
|
.roles
|
||||||
.items()
|
.items()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, _, v)| v.0.is_some())
|
.filter_map(|(k, _, v)| v.0.clone().map(|x| (k, x)))
|
||||||
.map(|(k, _, v)| (hex::encode(k), v.0.clone()))
|
.map(|(k, v)| NodeRoleResp {
|
||||||
.collect(),
|
id: hex::encode(k),
|
||||||
staged_role_changes: layout
|
zone: v.zone.clone(),
|
||||||
|
capacity: v.capacity,
|
||||||
|
tags: v.tags.clone(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let staged_role_changes = layout
|
||||||
.staging_roles
|
.staging_roles
|
||||||
.items()
|
.items()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(k, _, v)| layout.roles.get(k) != Some(v))
|
.filter(|(k, _, v)| layout.roles.get(k) != Some(v))
|
||||||
.map(|(k, _, v)| (hex::encode(k), v.0.clone()))
|
.map(|(k, _, v)| match &v.0 {
|
||||||
.collect(),
|
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,
|
||||||
|
staged_role_changes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----
|
||||||
|
|
||||||
#[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>>,
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue