Garage v0.9 #473

Merged
lx merged 175 commits from next into main 2023-10-10 13:28:29 +00:00
3 changed files with 175 additions and 75 deletions
Showing only changes of commit 52376d47ca - Show all commits

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