//! Node metadata helpers for Chainfire KVS //! //! This module provides helpers for storing and retrieving node metadata //! in the Chainfire distributed KVS. //! //! # KVS Key Schema //! //! Node metadata is stored with the following key structure: //! - `/nodes//info` - JSON-encoded NodeMetadata //! - `/nodes//roles` - JSON-encoded roles (raft_role, gossip_role) //! - `/nodes//capacity/cpu` - CPU cores (u32) //! - `/nodes//capacity/memory_gb` - Memory in GB (u32) //! - `/nodes//labels/` - Custom labels (string) //! - `/nodes//api_addr` - API address (string) use crate::error::Result; use crate::Client; use chainfire_types::node::NodeRole; use chainfire_types::RaftRole; use serde::{Deserialize, Serialize}; use std::collections::HashMap; /// Node metadata stored in KVS #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NodeMetadata { /// Unique node identifier pub id: u64, /// Human-readable node name pub name: String, /// Raft participation role pub raft_role: RaftRole, /// Gossip/cluster role pub gossip_role: NodeRole, /// API address for client connections pub api_addr: String, /// Raft address for inter-node communication (optional for workers) pub raft_addr: Option, /// Gossip address for membership protocol pub gossip_addr: String, /// Node capacity information pub capacity: NodeCapacity, /// Custom labels for node selection pub labels: HashMap, } /// Node capacity information #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct NodeCapacity { /// Number of CPU cores pub cpu_cores: u32, /// Memory in gigabytes pub memory_gb: u32, /// Disk space in gigabytes (optional) pub disk_gb: Option, } /// Filter for listing nodes #[derive(Debug, Clone, Default)] pub struct NodeFilter { /// Filter by Raft role pub raft_role: Option, /// Filter by gossip role pub gossip_role: Option, /// Filter by labels (all must match) pub labels: HashMap, } impl NodeMetadata { /// Create a new NodeMetadata for a control-plane node pub fn control_plane( id: u64, name: impl Into, api_addr: impl Into, raft_addr: impl Into, gossip_addr: impl Into, ) -> Self { Self { id, name: name.into(), raft_role: RaftRole::Voter, gossip_role: NodeRole::ControlPlane, api_addr: api_addr.into(), raft_addr: Some(raft_addr.into()), gossip_addr: gossip_addr.into(), capacity: NodeCapacity::default(), labels: HashMap::new(), } } /// Create a new NodeMetadata for a worker node pub fn worker( id: u64, name: impl Into, api_addr: impl Into, gossip_addr: impl Into, ) -> Self { Self { id, name: name.into(), raft_role: RaftRole::None, gossip_role: NodeRole::Worker, api_addr: api_addr.into(), raft_addr: None, gossip_addr: gossip_addr.into(), capacity: NodeCapacity::default(), labels: HashMap::new(), } } /// Set capacity information pub fn with_capacity(mut self, cpu_cores: u32, memory_gb: u32) -> Self { self.capacity.cpu_cores = cpu_cores; self.capacity.memory_gb = memory_gb; self } /// Add a label pub fn with_label(mut self, key: impl Into, value: impl Into) -> Self { self.labels.insert(key.into(), value.into()); self } } /// Key prefix for all node metadata const NODE_PREFIX: &str = "/nodes/"; /// Generate the key for node info fn node_info_key(id: u64) -> String { format!("{}{}/info", NODE_PREFIX, id) } /// Generate the key for a node label fn node_label_key(id: u64, label: &str) -> String { format!("{}{}/labels/{}", NODE_PREFIX, id, label) } /// Register a node in the cluster by storing its metadata in KVS /// /// # Arguments /// /// * `client` - The Chainfire client /// * `meta` - Node metadata to register /// /// # Returns /// /// The revision number of the write operation pub async fn register_node(client: &mut Client, meta: &NodeMetadata) -> Result { let key = node_info_key(meta.id); let value = serde_json::to_string(meta) .map_err(|e| crate::error::ClientError::Internal(e.to_string()))?; client.put_str(&key, &value).await } /// Update a specific node attribute pub async fn update_node_label( client: &mut Client, node_id: u64, label: &str, value: &str, ) -> Result { let key = node_label_key(node_id, label); client.put_str(&key, value).await } /// Get a node's metadata by ID /// /// # Arguments /// /// * `client` - The Chainfire client /// * `node_id` - The node ID to look up /// /// # Returns /// /// The node metadata if found, None otherwise pub async fn get_node(client: &mut Client, node_id: u64) -> Result> { let key = node_info_key(node_id); let value = client.get_str(&key).await?; match value { Some(json) => { let meta: NodeMetadata = serde_json::from_str(&json) .map_err(|e| crate::error::ClientError::Internal(e.to_string()))?; Ok(Some(meta)) } None => Ok(None), } } /// List all registered nodes /// /// # Arguments /// /// * `client` - The Chainfire client /// * `filter` - Optional filter criteria /// /// # Returns /// /// A list of node metadata matching the filter pub async fn list_nodes(client: &mut Client, filter: &NodeFilter) -> Result> { let prefix = format!("{}", NODE_PREFIX); let entries = client.get_prefix(&prefix).await?; let mut nodes = Vec::new(); for (key, value) in entries { let key_str = String::from_utf8_lossy(&key); // Only process /nodes//info keys if !key_str.ends_with("/info") { continue; } let json = String::from_utf8_lossy(&value); if let Ok(meta) = serde_json::from_str::(&json) { // Apply filters if let Some(ref raft_role) = filter.raft_role { if meta.raft_role != *raft_role { continue; } } if let Some(ref gossip_role) = filter.gossip_role { if meta.gossip_role != *gossip_role { continue; } } // Check label filters let mut labels_match = true; for (k, v) in &filter.labels { match meta.labels.get(k) { Some(node_v) if node_v == v => {} _ => { labels_match = false; break; } } } if labels_match { nodes.push(meta); } } } // Sort by node ID for consistent ordering nodes.sort_by_key(|n| n.id); Ok(nodes) } /// Unregister a node from the cluster /// /// # Arguments /// /// * `client` - The Chainfire client /// * `node_id` - The node ID to unregister /// /// # Returns /// /// True if the node was found and deleted pub async fn unregister_node(client: &mut Client, node_id: u64) -> Result { let key = node_info_key(node_id); client.delete(&key).await } #[cfg(test)] mod tests { use super::*; #[test] fn test_node_info_key() { assert_eq!(node_info_key(1), "/nodes/1/info"); assert_eq!(node_info_key(123), "/nodes/123/info"); } #[test] fn test_node_label_key() { assert_eq!(node_label_key(1, "zone"), "/nodes/1/labels/zone"); } #[test] fn test_control_plane_metadata() { let meta = NodeMetadata::control_plane( 1, "cp-1", "127.0.0.1:2379", "127.0.0.1:2380", "127.0.0.1:2381", ); assert_eq!(meta.id, 1); assert_eq!(meta.raft_role, RaftRole::Voter); assert_eq!(meta.gossip_role, NodeRole::ControlPlane); assert!(meta.raft_addr.is_some()); } #[test] fn test_worker_metadata() { let meta = NodeMetadata::worker(100, "worker-1", "127.0.0.1:3379", "127.0.0.1:3381"); assert_eq!(meta.id, 100); assert_eq!(meta.raft_role, RaftRole::None); assert_eq!(meta.gossip_role, NodeRole::Worker); assert!(meta.raft_addr.is_none()); } #[test] fn test_metadata_with_capacity() { let meta = NodeMetadata::worker(1, "worker", "addr", "gossip") .with_capacity(8, 32) .with_label("zone", "us-west-1"); assert_eq!(meta.capacity.cpu_cores, 8); assert_eq!(meta.capacity.memory_gb, 32); assert_eq!(meta.labels.get("zone"), Some(&"us-west-1".to_string())); } #[test] fn test_metadata_serialization() { let meta = NodeMetadata::control_plane(1, "test", "api", "raft", "gossip") .with_capacity(4, 16) .with_label("env", "prod"); let json = serde_json::to_string(&meta).unwrap(); let deserialized: NodeMetadata = serde_json::from_str(&json).unwrap(); assert_eq!(meta.id, deserialized.id); assert_eq!(meta.raft_role, deserialized.raft_role); assert_eq!(meta.capacity.cpu_cores, deserialized.capacity.cpu_cores); } }