use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use tracing::{debug, info};
use tatara_core::cluster::types::{NodeId, NodeMeta, NodeRoles};
use tatara_core::domain::job::{DriverType, Resources};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KindlingIdentity {
#[serde(default)]
pub version: String,
#[serde(default)]
pub profile: String,
#[serde(default)]
pub hostname: String,
#[serde(default)]
pub hardware: HardwareConfig,
#[serde(default)]
pub fleet: FleetConfig,
#[serde(default)]
pub network: NetworkConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct HardwareConfig {
#[serde(default)]
pub platform: String,
#[serde(default)]
pub cpu: CpuConfig,
#[serde(default)]
pub memory: Option<MemoryConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CpuConfig {
#[serde(default)]
pub cores: Option<u32>,
#[serde(default)]
pub threads: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryConfig {
pub size_gb: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct FleetConfig {
pub controller: Option<String>,
pub environment: Option<String>,
pub owner: Option<String>,
pub team: Option<String>,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub peers: Vec<FleetPeer>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FleetPeer {
pub name: String,
pub hostname: String,
#[serde(default = "default_ssh_user")]
pub ssh_user: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct NetworkConfig {
#[serde(default)]
pub interfaces: HashMap<String, NetworkInterface>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct NetworkInterface {
pub address: Option<String>,
}
fn default_ssh_user() -> String {
"root".to_string()
}
pub fn load_identity(path: Option<&Path>) -> Result<Option<KindlingIdentity>> {
let identity_path = path
.map(PathBuf::from)
.unwrap_or_else(|| default_identity_path());
if !identity_path.exists() {
debug!(path = %identity_path.display(), "Kindling identity file not found");
return Ok(None);
}
let content = std::fs::read_to_string(&identity_path)
.with_context(|| format!("Failed to read {}", identity_path.display()))?;
let identity: KindlingIdentity = serde_yaml::from_str(&content)
.with_context(|| format!("Failed to parse {}", identity_path.display()))?;
info!(
hostname = %identity.hostname,
profile = %identity.profile,
"Loaded kindling identity"
);
Ok(Some(identity))
}
pub fn derive_node_id(hostname: &str) -> NodeId {
use sha2::{Digest, Sha256};
let hash = Sha256::digest(hostname.as_bytes());
u64::from_le_bytes(hash[..8].try_into().unwrap())
}
pub fn build_node_meta(
identity: &KindlingIdentity,
roles: NodeRoles,
http_addr: &str,
gossip_addr: &str,
raft_addr: &str,
drivers: Vec<DriverType>,
) -> NodeMeta {
let cpu_mhz = identity
.hardware
.cpu
.threads
.or(identity.hardware.cpu.cores)
.unwrap_or_else(|| {
std::thread::available_parallelism()
.map(|p| p.get() as u32)
.unwrap_or(1)
}) as u64
* 1000;
let memory_mb = identity
.hardware
.memory
.as_ref()
.map(|m| (m.size_gb * 1024.0) as u64)
.unwrap_or(0);
NodeMeta {
node_id: derive_node_id(&identity.hostname),
hostname: identity.hostname.clone(),
http_addr: http_addr.to_string(),
gossip_addr: gossip_addr.to_string(),
raft_addr: raft_addr.to_string(),
os: std::env::consts::OS.to_string(),
arch: std::env::consts::ARCH.to_string(),
roles,
drivers,
total_resources: Resources { cpu_mhz, memory_mb },
available_resources: Resources { cpu_mhz, memory_mb },
allocations_running: 0,
joined_at: chrono::Utc::now(),
version: env!("CARGO_PKG_VERSION").to_string(),
eligible: true,
wireguard_pubkey: None,
tunnel_address: None,
}
}
pub fn fleet_seed_peers(fleet: &FleetConfig, gossip_port: u16) -> Vec<String> {
fleet
.peers
.iter()
.map(|p| format!("{}:{}", p.hostname, gossip_port))
.collect()
}
fn default_identity_path() -> PathBuf {
dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("~/.config"))
.join("kindling")
.join("node.yaml")
}